Introdução à Programação Java, Parte 2: Desenvolvimentos para Aplicativos Reais

Recursos mais avançados da linguagem Java

Na Parte 1 deste tutorial, o programador profissional de Java™, J. Steven Perry, apresentou a sintaxe e bibliotecas Java que você necessita para escrever aplicativos Java simples. A Parte 2, ainda voltada a desenvolvedores iniciantes no desenvolvimento de aplicativo Java, apresenta desenvolvimentos de programação mais sofisticados, necessários para desenvolver aplicativos Java complexos reais. Os tópicos abordados incluem a manipulação de exceções, herança e abstração, expressões regulares, genéricos, E/S Java e serialização de Java.

J Steven Perry, Principal Consultant, Makoto Consulting Group, Inc.

Photo of J Steven PerryJ. Steven Perry é um consultor de desenvolvimento de software independente e vem desenvolvendo software profissionalmente desde 1991. Steve tem paixão por desenvolvimento de software e gosta de escrever sobre desenvolvimento de software e de orientar outros desenvolvedores. Ele é autor de Java Management Extensions (O'Reilly) e Log4j (O'Reilly), e Joda-Time (que escreveu para o IBM developerWorks). Em seu tempo livre, ele sai com seus três filhos, anda de bicicleta e ensina ioga. Steve é o proprietário e o principal consultor do Makoto Consulting Group, localizado em Little Rock, Arkansas.



17/Mai/2011

Antes de iniciar

Descubra o que esperar deste tutorial e como aproveitá-lo ao máximo.

Sobre esta série

O tutorial "Introdução à Programação Java" de duas partes destina-se a deixar os desenvolvedores de software iniciantes na tecnologia Java atualizados e preparados para executar programação orientada a objetos (OOP) e desenvolvimento de aplicativo real usando a linguagem e a plataforma Java.

Sobre este tutorial

Esta segunda parte do tutorial "Introdução à Programação Java" apresenta recursos da linguagem Java mais sofisticados em relação aos abordados na Parte 1.

Objetivos

O idioma Java é maduro e sofisticado o suficiente para ajudá-lo a realizar praticamente qualquer tarefa de programação. Neste tutorial, você será apresentado aos recursos da linguagem Java que precisará para manipular cenários de programação complexos, inclusive:

  • Manipulação de exceção
  • Herança e abstração
  • Interfaces
  • Classes aninhadas
  • Expressões regulares
  • Genéricos
  • Tipos enum
  • E/S
  • Serialização

Pré-requisitos

O conteúdo deste tutorial destina-se a programadores iniciantes na linguagem Java que não estão familiarizados com seus recursos mais sofisticados. O tutorial parte da premissa de que você estudou a "Introdução à Programação Java, Parte 1: Elementos Básicos da Linguagem Java " para:

  • Obter entendimento dos elementos básicos da OOP na plataforma Java.
  • Configurar o ambiente de desenvolvimento para os exemplos do tutorial.
  • Iniciar o projeto de programação que continuará a desenvolver na Parte 2.

Requisitos do Sistema

Os exercícios neste tutorial requerem um ambiente de desenvolvimento composto por:

  • JDK 6 da Sun/Oracle
  • Eclipse IDE para Desenvolvedores de Java

As instruções de download e instalação para ambos estão incluídas na Parte 1.

A configuração do sistema recomendada para este tutorial é:

  • Um sistema que suporte JDK 6 com, pelo menos, 1 GB de memória principal. O Java 6 é suportado em Linux®, Windows®e Solaris®.
  • Pelo menos, 20 MB de espaço em disco para instalar os componentes de software e exemplos abrangidos.

Próximas Etapas com Objetos

A Parte 1 deste tutorial terminou com um objeto Person que era razoavelmente útil, mas não tão útil quando poderia ser. Agora, você começará a aprender técnicas de aprimoramento de um objeto, como Person, iniciando com as seguintes técnicas:

  • Sobrecarregando métodos
  • Substituindo métodos
  • Comparando um objeto a outro
  • Tornando seu código mais fácil de depurar

Sobrecarregando Métodos

Quando você cria dois métodos com o mesmo nome, mas com diferentes listas de argumentos (ou seja, números ou tipos diferentes de parâmetros), você tem um método sobrecarregado. Os métodos sobrecarregados estão sempre na mesma classe. No tempo de execução, o Java Runtime Environment (JRE; também conhecido como Java Runtime) decide que variação do método sobrecarregado chamar com base nos argumentos que foram fornecidos a ele.

Suponha que Person precise de alguns métodos para imprimir uma auditoria do seu status atual. Chamarei esses métodos de printAudit(). Cole o método sobrecarregado na Listagem 1, na visualização do editor do Eclipse:

Listagem 1. printAudit(): Um Método Sobrecarregado
public void printAudit(StringBuilder buffer) {
  buffer.append("Name="); buffer.append(getName());
  buffer.append(","); buffer.append("Age="); buffer.append(getAge());
  buffer.append(","); buffer.append("Height="); buffer.append(getHeight());
  buffer.append(","); buffer.append("Weight="); buffer.append(getWeight());
  buffer.append(","); buffer.append("EyeColor="); buffer.append(getEyeColor());
  buffer.append(","); buffer.append("Gender="); buffer.append(getGender());
}
public void printAudit(Logger l) {
  StringBuilder sb = new StringBuilder();
  printAudit(sb);
  l.info(sb.toString());
}

Nesse caso, há duas versões sobrecarregadas de printAudit(), e uma realmente usa a outra. Fornecendo duas versões, você dá ao responsável pela chamada uma opção de como imprimir uma auditoria da classe. Dependendo dos parâmetros informados, o Java Runtime chamará o método correto.

Duas Regras de Sobrecarga de Método

Lembre-se de duas regras importantes ao usar métodos sobrecarregados:

  • Não é possível sobrecarregar um método apenas alterando seu tipo de retorno.
  • Não é possível ter dois métodos com a mesma lista de parâmetros.

Se você violar essas regras, o compilador obterá um erro.

Substituindo Métodos

Quando uma subclasse de outra classe fornece sua própria implementação de um método definido em uma classe-pai, isso é conhecido como substituição de método. Para ver como a substituição de método é útil, é necessário fazer algum trabalho na classe Employee. Uma vez configurada essa classe, será possível mostrar onde a substituição de método é útil.

Employee: Uma subclasse de Person

Lembre-se da Parte 1 deste tutorial que Employee pode ser uma subclasse (ou filha) de Person e ter alguns atributos adicionais:

  • Número de identificação do contribuinte
  • Número do funcionário
  • Data da contratação
  • Salário

Para declarar essa classe em um arquivo denominado Employee.java, clique com o botão direito no pacote com.makotogroup.intro, no Eclipse. Escolha New > Class..., e a caixa de diálogo New Java Class se abrirá, como mostrado na figura 1:

Figura 1. Diálogo New Java Class
Diálogo New Java Class

Digite Employee como o nome da classe e Person como sua superclasse, e clique em Finish. Você verá a classe Employee em uma janela de edição. Não é necessário declarar explicitamente um construtor, mas continue e implemente ambos os construtores de qualquer maneira. Primeiro, com o foco na janela de edição da classe Employee, vá para Source > Generate Constructors from Superclass... e você verá um diálogo semelhante ao exibido na figura 2:

Figura 2. Diálogo Generate Constructors from Superclass
Diálogo Generate Constructors from Superclass

Verifique ambos os construtores (como mostrados na figura 2) e clique em OK. O Eclipse gerará os construtores para você. Agora, você deve ter uma classe Employee como a ilustrada na listagem 2:

Listagem 2. A nova classe Employee aprimorada
package com.makotogroup.intro;

public class Employee extends Person {

  public Employee() {
    super();
    // TODO Auto-generated constructor stub
  }

  public Employee(String name, int age, int height, int weight,
                  String eyeColor, String gender) {
    super(name, age, height, weight, eyeColor, gender);
    // TODO Auto-generated constructor stub
  }

}

Employee é herdada de Person

Employee herda os atributos e o comportamento do seu pai, Person, e também tem alguns atributos e comportamentos próprios, como pode ser visto na listagem 3:

Listagem 3. A classe Employee com os atributos de Person
package com.makotogroup.intro;


import java.math.BigDecimal;

public class Employee extends Person {

  private String taxpayerIdentificationNumber;
  private String employeeNumber;
  private BigDecimal salary;

  public Employee() {
    super();
  }
  public String getTaxpayerIdentificationNumber() {
    return taxpayerIdentificationNumber;
  }
  public void setTaxpayerIdentificationNumber(String taxpayerIdentificationNumber) {
    this.taxpayerIdentificationNumber = taxpayerIdentificationNumber;
  }
  // Other getter/setters...
}

Substituição de método: printAudit()

Agora, como prometido, você está pronto para um exercício sobre substituição de métodos. Você substituirá o método printAudit() (consulte a listagem 1) usado para formatar o estado atual de uma instância Person. Employee herda esse comportamento de Person, e se você instanciar Employee, definir seus atributos e chamar uma das sobrecargas de printAudit(), a chamada será bem-sucedida. No entanto, a auditoria produzida não representará completamente um Employee. O problema é que ela não poderá formatar os atributos específicos para um Employee, porque Person não sabe nada sobre eles.

A solução é substituir a sobrecarga de printAudit() que usa um StringBuilder como parâmetro e incluir código para imprimir os atributos específicos para Employee.

Para fazer isso no seu Eclipse IDE, vá para Source > Override/Implement Methods..., e você verá uma caixa de diálogo parecida com a figura 3:

Figura 3. Diálogo Override/Implement Methods
Diálogo Override/Implement Methods

Selecione a sobrecarga de printAudit de StringBuilder, como mostrado na figure 3, e clique em OK. O Eclipse gerará o stub de método para você e então será possível apenas preencher o resto, por exemplo:

@Override
public void printAudit(StringBuilder buffer) {
  // Call the superclass version of this method first to get its attribute values
  super.printAudit(buffer);
  // Now format this instance's values
  buffer.append("TaxpayerIdentificationNumber=");
    buffer.append(getTaxpayerIdentificationNumber());
  buffer.append(","); buffer.append("EmployeeNumber=");
    buffer.append(getEmployeeNumber());
  buffer.append(","); buffer.append("Salary=");
    buffer.append(getSalary().setScale(2).toPlainString());
}

Observe a chamada para super.printAudit(). O que você está fazendo aqui é solicitar à superclasse (Person) para exibir seu comportamento para printAudit(), e então aumentá-lo com o comportamento de um printAudit() do tipo Employee.

A chamada para super.printAudit() não precisa ser a primeira; apenas parece ser uma boa ideia imprimir esses atributos primeiro. Na verdade, não é necessário chamar super.printAudit() afinal. Se você não chamá-la, deverá formatar os atributos de Person por conta própria (no método Employee.printAudit()), ou excluí-los completamente. Fazer uma chamada para super.printAudit(), nesse caso, é mais fácil.

Membros da Classe

As variáveis e os métodos que você tem em Person e em Employee são as variáveis e os métodos da instância. Para usá-los, é necessário instanciar a classe que você precisa ou ter uma referência à instância. Cada instância de objeto tem variáveis e métodos, e para cada comportamento exato (por exemplo, o que foi gerado pela chamada de printAudit()) será diferente, porque se baseia no estado da instância do objeto.

As classes em si também podem ter variáveis e métodos, que são chamados de membros da classe. Você declara os membros de classe com a palavra-chave static apresentada na Parte 1 deste tutorial. As diferenças entre os membros da classe e os membros da instância são:

  • Cada instância de uma classe compartilha uma única cópia de uma variável de classe.
  • É possível chamar métodos na própria classe, sem ter uma instância.
  • Os métodos da instância podem acessar as variáveis da classe, mas os métodos da classe não podem acessar as variáveis da instância.
  • Os métodos da classe podem acessar apenas variáveis da classe.

Incluindo Variáveis e Métodos de Classe

Qual o momento apropriado para incluir variáveis e métodos de classe? A melhor dica é fazer isso raramente, de modo a não utilizar esse recurso em demasia. Com isso em mente, é uma boa ideia usar variáveis e métodos de classe:

  • Para declarar constantes que qualquer instância de classe possa usar (e cujo valor esteja fixado no tempo de desenvolvimento).
  • Para controlar os "contadores" das instâncias da classe.
  • Em uma classe com métodos de utilitário que nunca precisará de uma instância de classe (como Logger.getLogger()).

Variáveis de Classe

Para criar uma variável de classe , use a palavra-chave static ao declará-la:

accessSpecifier static variableName [= initialValue];

Nota: Os colchetes retos aqui indicam que o conteúdo é opcional. Eles não são parte da sintaxe da declaração.

A JRE cria espaço na memória para armazenar cada uma das variáveis de instância de classe para cada instância dessa classe. Em contraste, o JRE cria apenas uma única cópia de cada variável de classe, independentemente do número de instâncias. Ele faz isso a primeira vez quando a classe é carregada (ou seja, a primeira vez que ela encontra a classe em um programa). Todas as classes compartilharão essa única cópia da variável. Isso torna as variáveis de classe uma boa opção para as constantes que todas as instâncias devem ser capazes de usar.

Por exemplo, você declarou o atributo Gender de Person como uma String, mas não colocou nenhuma restrição ao redor dele. A listagem 4 mostra o uso comum das variáveis de classe:

Listagem 4. Usando variáveis de classe
public class Person {
//. . .
 public static final String GENDER_MALE = "MALE";
 public static final String GENDER_FEMALE = "FEMALE";
// . . .
 public static void main(String[] args) {
   Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", GENDER_MALE);
 // . . .
 }
//. . .
}

Declarando Constantes

Normalmente, as constantes são:

  • Nomeadas com todos os caracteres maiúsculos.
  • Nomeadas com várias palavras, separadas por sublinhados.
  • Declaradas final (de modo que seus valores não possam ser modificados).
  • Declaradas com um especificador de acesso public (de modo que elas possam ser acessadas por outras classes que necessitem fazer referência a seus valores por nome).

Na listagem 4, para usar a constante para MALE na chamada do construtor Person, basta fazer uma simples referência a seu nome. Para usar uma constante fora da classe, você a prefaciou com o nome da classe onde ela estava declarada, como o que segue:

String genderValue = Person.GENDER_MALE;

Métodos de Classe

Se você está acompanhando este texto desde a Parte 1, já chamou o método estático Logger.getLogger() várias vezes — sempre que teve de recuperar a instância Logger para escrever alguma saída para o console. Observe, no entanto, que não foi necessária uma instância de Logger para fazer isso; em vez disso, você fez referência à própria classe Logger. Essa é a sintaxe para fazer uma chamada de método de classe. Como com as variáveis de classe, a palavra-chave static identifica Logger (neste exemplo) como um método de classe. Os métodos de classe também são chamados, algumas vezes, de métodos estáticos por essa razão.

Usando Métodos de Classe

Agora você combinará o que aprendeu sobre variáveis e métodos estáticos para criar um método estático em Employee. Você declarará uma variável private static final para manter um Logger, que todas as instâncias compartilharão, e que será acessível chamando getLogger() na classe Employee. A listagem 5 mostra como:

Listagem 5. Criando um método de classe (ou estático)
public class Employee extends Person {
 private static final Logger logger = Logger.getLogger(Employee.class.getName());
//. . .
 public static Logger getLogger() {
   return logger;

 }

}

Duas coisas importantes estão acontecendo na listagem 5:

  • A instância Logger está declarada com o acesso private, assim nenhuma classe fora de Employee pode acessar a referência diretamente.
  • O Logger será inicializado quando a classe for carregada; isso se dá porque você usa a sintaxe do inicializador Java para atribuí-lo a um valor.

Para recuperar o objeto Logger da classe Employee, faça a seguinte chamada:

Logger employeeLogger = Employee.getLogger();

Comparando Objetos

A linguagem Java fornece dois meios para comparar objetos:

  • O operador ==
  • O método equals()

Comparando Objetos com ==

A sintaxe == compara objetos quanto à igualdade, assim a == b retornará true somente se a e b tiverem o mesmo valor. No caso de objetos, isso significa que os dois se referem à mesma instância de objeto. No caso de primitivas, isso significa que os valores são idênticos. Considere o exemplo na listagem 6:

Listagem 6. Comparando objetos com ==
int int1 = 1;
int int2 = 1;
l.info("Q: int1 == int2?           A: " + (int1 == int2));

Integer integer1 = Integer.valueOf(int1);
Integer integer2 = Integer.valueOf(int2);
l.info("Q: Integer1 == Integer2?   A: " + (integer1 == integer2));

integer1 = new Integer(int1);
integer2 = new Integer(int2);
l.info("Q: Integer1 == Integer2?   A: " + (integer1 == integer2));

Employee employee1 = new Employee();
Employee employee2 = new Employee();
l.info("Q: Employee1 == Employee2? A: " + (employee1 == employee2));

Se você executar o código da listagem 6 dentro do Eclipse, a saída deverá ser:

Apr 19, 2010 5:30:10 AM com.makotogroup.intro.Employee main
INFO: Q: int1 == int2?           A: true
Apr 19, 2010 5:30:10 AM com.makotogroup.intro.Employee main
INFO: Q: Integer1 == Integer2?   A: true
Apr 19, 2010 5:30:10 AM com.makotogroup.intro.Employee main
INFO: Q: Integer1 == Integer2?   A: false
Apr 19, 2010 5:30:10 AM com.makotogroup.intro.Employee main
INFO: Q: Employee1 == Employee2? A: false

No primeiro caso na listagem 6, os valores das primitivas são idênticos, assim o operador == retorna true. No segundo caso, os objetos Integer se referem à mesma instância, assim, novamente, == retorna true. No terceiro caso, embora os objetos Integer encapsulem o mesmo valor, == retorna false porque integer1 e integer2 se referem a objetos diferentes. Com base nisso, deve estar claro o porquê employee1 == employee2 retorna false.

Comparando Objetos com equals()

equals() é um método que todo objeto da linguagem Java obtém gratuitamente, porque é definido como um método de instância de java.lang.Object (a partir do qual o objeto Java é herdado).

Você chama equals() exatamente como qualquer outro método:

a.equals(b);

Essa instrução chama o método equals() do objeto a, passando a ele uma referência para o objeto b. Por padrão, um programa Java faz simplesmente uma verificação para confirmar se dois objetos estão usando a mesma sintaxe ==. Como equals() é um método, ele pode ser substituído. Considere o exemplo da listagem 6, modificado na listagem 7 para comparar os dois objetos usando equals():

Listagem 7. Comparando objetos com equals()
Logger l = Logger.getLogger(Employee.class.getName());

Integer integer1 = Integer.valueOf(1);
Integer integer2 = Integer.valueOf(1);
l.info("Q: integer1 == integer2?        A: " + (integer1 == integer2));
l.info("Q: integer1.equals(integer2)?   A: " + integer1.equals(integer2));

integer1 = new Integer(integer1);
integer2 = new Integer(integer2);
l.info("Q: integer1 == integer2?        A: " + (integer1 == integer2));
l.info("Q: integer1.equals(integer2)?   A: " + integer1.equals(integer2));

Employee employee1 = new Employee();
Employee employee2 = new Employee();
l.info("Q: employee1 == employee2?      A: " + (employee1 == employee2));
l.info("Q: employee1.equals(employee2)? A: " + integer1.equals(integer2));
Running this code produces:

Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: integer1 == integer2?        A: true
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: integer1.equals(integer2)?   A: true
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: integer1 == integer2?        A: false
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: integer1.equals(integer2)?   A: true
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: employee1 == employee2?      A: false
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: employee1.equals(employee2)? A: false

Uma Nota sobre Como Comparar Integers

Na listagem 7, não deve haver nenhuma surpresa se o método equals() do Integer retornar true e == retornar true; mas observe o que acontece no segundo caso, quando você cria objetos separados que encapsulam o valor 1: == retorna false porque integer1 e integer2 se referem a objetos diferentes; mas equals() retorna true.

Os autores do JDK decidiram que para Integer, o significado de equals() seria diferente do padrão (que é comparar as referências de objeto para ver se elas se referem ao mesmo objeto), e que ele retornará true sempre que o valor int subjacente for idêntico.

Para Employee, você não substituiu equals(), assim o comportamento padrão (de usar ==) retornará o esperado, dado que employee1 e employee2 fazem referência a objetos diferentes.

Basicamente, isso significa que para qualquer objeto que você escrever, poderá definir os significados de equals() apropriados para o aplicativo que você está escrevendo.

Substituindo equals()

É possível definir o que equals() significa para os objetos do seu aplicativo substituindo o comportamento padrão de Object.equals(). Novamente, você pode usar o Eclipse para fazer isso. Assegure que o foco de Employee esteja na janela Eclipse IDE's Source e vá para Source > Override/Implement Methods. A caixa de diálogo na figura 4 aparecerá:

Figura 4. Diálogo Override/Implement Methods
Diálogo Override/Implement Methods

Você usou esse diálogo antes, mas neste caso deseja implementar o método da superclasse Object.equals(). Assim, localize Object na lista, marque o método equals(Object) e clique em OK. O Eclipse gerará o código correto e o substituirá no seu arquivo de origem.

Faz sentido que os dois objetos Employee sejam iguais quando os estados desses objetos forem iguais. Ou seja, eles serão iguais se os seus valores — último nome, primeiro nome, idade — forem iguais.

Gerando equals() Automaticamente

O Eclipse poderá gerar um método equals() para você com base nas variáveis da instância (atributos) que você definir para a classe. Como Employee é uma subclasse de Person, você gerará primeiro equals() para Person. Na visualização do Explorador de Projetos do Eclipse, clique com o botão direito do mouse em Person e escolha Generate hashCode() and equals() para abrir a caixa de diálogo mostrada na figura 5:

Figura 5. Diálogo Generate hashCode() and equals()
Diálogo Generate hashCode() and equals()

Selecione todos os atributos (como mostrado na Figura 5) e clique em OK. O Eclipse gerará um método equals() parecido com o presente na listagem 8:

Listagem 8. Um método equals() gerado pelo Eclipse
@Override
public boolean equals(Object obj) {
 if (this == obj)
   return true;
 if (obj == null)
   return false;
 if (getClass() != obj.getClass())
   return false;
 Person other = (Person) obj;
 if (age != other.age)
  return false;
 if (eyeColor == null) {
   if (other.eyeColor != null)
     return false;
 } else if (!eyeColor.equals(other.eyeColor))
   return false;
 if (gender == null) {
   if (other.gender != null)
     return false;
 } else if (!gender.equals(other.gender))
   return false;
 if (height != other.height)
   return false;
 if (name == null) {
   if (other.name != null)
     return false;
 } else if (!name.equals(other.name))
   return false;
 if (weight != other.weight)
   return false;
 return true;
}

Não se preocupe com hashCode() por agora — é possível mantê-lo ou excluí-lo. O método equals() gerado pelo Eclipse parece complicado, mas o que ele faz é muito simples: se o objeto informado for o mesmo objeto que aquele na listagem 8, então equals() retornará true. Se o objeto informado for nulo, ele retornará false.

Depois disso, o método faz uma verificação para confirmar se os objetos Class são idênticos (significando que o objeto informado deve ser um objeto Person). Se isso for verdadeiro, então cada valor de atributo do objeto transmitido será verificado para ver se ele corresponde valor a valor ao estado da instância Person dada. Se os valores dos atributos forem nulos (significando que estão ausentes) então equals() fará tantas verificações quanto possível e, se não encontrar nenhuma correspondência, os objetos serão considerados iguais. Talvez você não queira esse comportamento para cada programa, mas ele funciona para a maioria dos propósitos.

Exercício: Gerar um equals() para Employee

Tente as seguintes etapas em Gerando equals() Automaticamente para gerar um equals() para um Employee. Uma vez gerado o equals(), inclua o seguinte código acima dele:

public static void main(String[] args) {
 Logger l = Logger.getLogger(Employee.class.getName());

 Employee employee1 = new Employee();
 employee1.setName("J Smith");
 Employee employee2 = new Employee();
 employee2.setName("J Smith");
 l.info("Q: employee1 == employee2?      A: " + (employee1 == employee2));
 l.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));
}

Se você executar o código, deverá ver a seguinte saída:

Apr 19, 2010 5:26:50 PM com.makotogroup.intro.Employee main
INFO: Q: employee1 == employee2?      A: false
Apr 19, 2010 5:26:50 PM com.makotogroup.intro.Employee main
INFO: Q: employee1.equals(employee2)? A: true

Nesse caso, uma única correspondência em Name foi suficiente para convencer equals() que dois objetos eram iguais. Sinta-se livre para incluir mais atributos neste exemplo e ver o resultado.

Exercício: Substituir toString()

Lembra-se do método printAudit() do início desta seção? Se você achou que seu funcionamento era um pouco rígido demais, estava certo! Formatar o estado de um objeto em uma String é um padrão tão comum que os designers da linguagem Java o desenvolveram exatamente no próprio Object, em um método denominado (evidentemente) toString(). A implementação padrão de toString() não é muito útil, mas cada objeto tem uma. Neste exercício, você tornará o toString() padrão um pouco mais útil.

Se você suspeita que o Eclipse pode gerar-lhe um método toString(), está correto. Volte ao Explorador de Projetos, clique com o botão direito do mouse na classe Employee e escolha Source > Generate toString().... Você verá uma caixa de diálogo similar àquela presente na figura 5. Escolha todos os atributos e clique em OK. O código gerado pelo Eclipse para Employee é mostrado na listagem 9:

Listagem 9. Um método toString() gerado pelo Eclipse
@Override
public String toString() {
 return "Employee [employeeNumber=" + employeeNumber + ", salary=" + salary
 + ", taxpayerIdentificationNumber=" + taxpayerIdentificationNumber
 + "]";
}

O código que o Eclipse gera para toString não inclui o toString() da superclasse (a superclasse de Employee sendo Person). É possível corrigir isso rapidamente, usando o Eclipse com essa substituição:

@Override
public String toString() {
 return super.toString() +
   "Employee [employeeNumber=" + employeeNumber + ", salary=" + salary
   + ", taxpayerIdentificationNumber=" + taxpayerIdentificationNumber
   + "]";
}

A adição de toString() torna printAudit() muito mais simples:

@Override
public void printAudit(StringBuilder buffer) {
 buffer.append(toString());
}

toString() agora faz o trabalho pesado de formatação do estado atual do objeto, e você simplesmente acumula o que ele retorna no StringBuilder e no retorno.

Eu recomendo sempre implementar toString() em suas classes, só se for para fins de suporte. É virtualmente inevitável que, a esta altura, você queira ver qual o estado de um objeto enquanto o seu aplicativo está em execução, e toString() é um grande gancho para fazer isso.


Exceções

Não há programa que funcione 100% do tempo e os designers da linguagem Java sabem disso. Nesta seção, aprenda sobre os mecanismos integrados da plataforma Java para tratar das situações em que o código não funciona exatamente como planejado.

Conceitos Básicos da Manipulação de Exceção

Uma exceção é um evento que ocorre durante a execução do programa que interrompe o fluxo normal das instruções do programa. A manipulação de exceção é uma técnica essencial da programação Java. Essencialmente, você encapsula seu código em um bloco try (que significa "tentar e ver se causa alguma exceção"), e usa isso para capturar (catch) vários tipos de exceções.

Para iniciar com a manipulação de exceção, examine o código na listagem 10:

Listagem 10. Você vê o erro?
// . . .
public class Employee extends Person {
// . . .
 private static Logger logger;// = Logger.getLogger(Employee.class.getName());

 public static void main(String[] args) {
   Employee employee1 = new Employee();
   employee1.setName("J Smith");
   Employee employee2 = new Employee();
   employee2.setName("J Smith");
   logger.info("Q: employee1 == employee2?      A: " + (employee1 == employee2));
   logger.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));

 }

Observe que o inicializador para a variável estática mantém comentada a linha de referência a Logger. Execute esse código e você obterá a seguinte saída:

Exception in thread "main" java.lang.NullPointerException
 at com.makotogroup.intro.Employee.main(Employee.java:54)

Essa saída está informando que você está tentando fazer referência a um objeto que não está lá, e isso é um erro de desenvolvimento muito sério. Felizmente, você pode usar blocos try e catch para capturá-lo (junto com uma pequena ajuda de finally).

try, catche finally

A listagem 11 mostra o código sem os erros da listagem 10 com os blocos de código padrão para manipulação de exceção (try, catch e finally):

Listagem 11. Capturando uma exceção
// . . .
public class Employee extends Person {
// . . .
 private static Logger logger;// = Logger.getLogger(Employee.class.getName());

 public static void main(String[] args) {
   try {
     Employee employee1 = new Employee();
     employee1.setName("J Smith");
     Employee employee2 = new Employee();
     employee2.setName("J Smith");
     logger.info("Q: employee1 == employee2?      A: " + (employee1 == employee2));
     logger.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));
   } catch (NullPointerException npe) {
     // Handle...
     System.out.println("Yuck! Outputting a message with System.out.println() " +
                        "because the developer did something dumb!");
   } finally {
     // Always executes
   }
 }

Juntos, os blocos try, catch e finally formam uma rede de captura de exceções. Primeiro, a instrução try encapsula o código que pode emitir uma exceção. Se a exceção for emitida, a execução será interrompida imediatamente no bloco catch ou no manipulador de exceções. Quando todas as tentativas e capturas estão concluídas, a execução continua com o bloco finally, quer a exceção seja emitida, quer não. Ao capturar uma exceção, você tem a opção de tentar se recuperar graciosamente dele ou de sair do programa (ou método).

Na listagem 11, o programa se recupera do erro e gera uma mensagem de saída informando o que aconteceu.

A Hierarquia da Exceção

A linguagem Java incorpora uma hierarquia de exceções inteira, composta de muitos tipos de exceções, agrupadas em duas categorias principais:

  • As exceções verificadas são verificadas pelo compilador (o que significa que ele se certificará de que as exceções sejam manipuladas em algum lugar dentro do código).
  • As exceções não verificadas (também chamadas de exceções de tempo de execução) não são verificadas pelo compilador.

Quando um programa causa uma exceção, você diz que ele emitiu a exceção. Uma exceção verificada é declarada para o compilador por algum método que tenha a palavra-chave throws na sua assinatura de método. Isso é seguido por uma lista separada por vírgula de exceções que o método pode potencialmente emitir durante o curso da sua execução. Se o seu código chamar um método que especifica que ele emitirá um ou mais tipos de exceções, isso terá de ser manipulado de alguma forma, ou um throws terá de ser incluído na sua assinatura de método para repassar esse tipo de exceção.

No caso de uma exceção, o tempo de execução da linguagem Java procurará por um manipulador de exceções em algum lugar acima na pilha. Se ele não encontrar nenhum e chegar ao topo da pilha, ele parará o programa abruptamente, como você viu na listagem 10.

Vários Blocos catch

É possível ter vários blocos catch, mas eles devem estar estruturados de um modo especial. Se alguma exceção for subclasse de outras exceções, as classes-filha serão colocadas à frente das classes-pai na ordem dos blocos catch. Este é um exemplo:

try {
 // Code here...
} catch (NullPointerException e) {
 // Handle NPE...
} catch (Exception e) {
 // Handle more general exception here...
}

Nesse exemplo, NullPointerException é uma classe-filha (eventualmente) de Exception, assim ela deve ser colocada à frente do bloco mais geral Exception catch.

Neste tutorial, você teve apenas uma pequena noção da manipulação de exceção do Java. Apenas este tópico poderia ser tema do seu próprio tutorial. Consulte Recursos para aprender mais sobre a manipulação de exceção em programas Java.


Desenvolvendo Aplicativos Java

Nesta seção, você continuará a desenvolver um Person como um aplicativo Java. Ao longo do caminho, obterá uma ideia melhor de como um objeto (ou uma coleção de objetos) evolui para um aplicativo.

Elementos de um Aplicativo Java

Todos os aplicativos Java necessitam de um ponto de entrada onde o tempo de execução Java saiba iniciar a execução do código. Esse ponto de entrada é o método main(). Os objetos do domínio, normalmente, não têm métodos main(), mas, pelo menos, uma classe em cada aplicativo deve tê-lo.

Você trabalhou desde a Parte 1 no exemplo de um aplicativo de recursos humanos que inclui Person e suas subclasses Employee. Agora, vejamos o que acontece quando você inclui uma nova classe no aplicativo.

Criando uma Classe do Driver

A finalidade de uma classe do driver é, como o nome sugere, "conduzir" um aplicativo. Observe que esse driver simples do aplicativo de recursos humanos contém um método main():

package com.makotogroup.intro;
public class HumanResourcesApplication {
 public static void main(String[] args) {
 }
}

Crie uma classe do driver no Eclipse usando o mesmo procedimento usado para criar Person e Employee. Atribua-lhe o nome HumanResourcesApplication e não se esqueça de selecionar a opção para incluir um método main() na classe. O Eclipse gerará a classe para você.

Inclua algum código no seu novo main() de modo que ele se pareça com o que segue:

public static void main(String[] args) {
 Employee e = new Employee();
 e.setName("J Smith");
 e.setEmployeeNumber("0001");
 e.setTaxpayerIdentificationNumber("123-45-6789");
 e.printAudit(log);
}

Agora, ative a classe HumanResourcesApplication e acompanhe sua execução. Você deverá ver esta saída (as barras invertidas aqui indicam uma continuação de linha):

Apr 29, 2010 6:45:17 AM com.makotogroup.intro.Person printAudit
INFO: Person [age=0, eyeColor=null, gender=null, height=0, name=J Smith,\
weight=0]Employee [employeeNumber=0001, salary=null,\
taxpayerIdentificationNumber=123-45-6789]

Isso é tudo o que é realmente necessário para se criar um aplicativo Java simples. Na próxima seção, começaremos a examinar alguma sintaxe e bibliotecas que o ajudarão a desenvolver aplicativos mais complexos.


Herança

Você já se deparou com alguns exemplos de herança algumas vezes neste tutorial. Esta seção recapitula algum material da Parte 1 sobre herança e explica em mais detalhes como ela funciona — incluindo a hierarquia da herança, os construtores e a herança, e a abstração da herança.

Como a Herança Funciona

As classes no código Java existem em hierarquias. As classes acima de determinada classe em uma hierarquia são suas superclasses. Essa classe, em especial, é uma subclasse de cada classe acima na hierarquia. Uma subclasse é herdada das suas superclasses. A classe java.lang.Object está no topo da hierarquia de classes, o que significa que toda classe Java é uma subclasse de Object e foi herdada dela.

Por exemplo, suponha que você tenha uma classe Person que se pareça com aquela presente na listagem 12:

Listagem 12. Classe Person pública
package com.makotogroup.intro;

// . . .
public class Person {
 public static final String GENDER_MALE = "MALE";
 public static final String GENDER_FEMALE = "FEMALE";
 public Person() {
 //Nothing to do...
 }
 private String name;
 private int age;
 private int height;
 private int weight;
 private String eyeColor;
 private String gender;
// . . .

}

A classe Person na listagem 12 foi herdada implicitamente da Object. Como isso é presumido para todas as classes, não será necessário digitar extends Object para cada classe que definir. Mas, o que significa dizer que uma classe é herdada de sua superclasse? Isso significa, simplesmente, que Person tem acesso às variáveis e aos métodos expostos nas suas superclasses. Neste caso, Person pode ver e usar os métodos e variáveis públicos de Object e os métodos e variáveis protegidos de Object.

Definindo uma Hierarquia de Classes

Agora, suponha que você tenha uma classe Employee que seja herdada de Person. Sua definição de classe (ou gráfico de herança) se parece como esta:

public class Employee extends Person {

 private String taxpayerIdentificationNumber;
 private String employeeNumber;
 private BigDecimal salary;
// . . .
}

Herança Múltipla versus Herança Única

Linguagens como a C++ suportam o conceito de herança múltipla: em qualquer ponto na hierarquia, uma classe pode ser herdada de uma ou mais classes. A linguagem Java suporta apenas herança única, o que significa que a palavra-chave extends pode ser usada apenas com uma única classe. Assim, a hierarquia de classes de determinada classe Java é sempre composta de uma linha reta para cima, até o java.lang.Object.

No entanto, a linguagem Java suporta a implementação de várias interfaces em uma única classe, o que serve como uma solução alternativa quando se trata de herança única. Eu lhe apresentarei às interfaces múltiplas posteriormente neste tutorial.

O gráfico de herança de Employee implica que Employee tem acesso a todas as variáveis e métodos, públicos e protegidos, em Person (visto ser uma extensão direta), assim como em Object (que é, de fato, uma extensão dessa classe, ainda que indiretamente). No entanto, como Employee e Person estão no mesmo pacote, Employee também tem acesso às variáveis e aos métodos privados do pacote (algumas vezes, chamado de amigáveis) em Person.

Para ir uma etapa mais fundo na hierarquia de classes, você criará uma terceira classe que estende Employee:

public class Manager extends Employee {
// . . .
}

Na linguagem Java, qualquer classe pode ter no máximo uma superclasse, mas uma classe pode ter um número ilimitado de subclasses. Ou seja, a coisa mais importante é lembrar-se da hierarquia de herança na linguagem Java.

Os Construtores e a Herança

Os construtores não são membros completos orientados a objetos, assim eles não são herdados; em vez disso, é necessário implementá-los explicitamente nas subclasses. Antes de fazer isso, vamos relembrar algumas regras básicas sobre como os construtores são definidos e chamados.

Conceitos Básicos sobre Construtores

Lembre-se de que um construtor sempre tem o mesmo nome da classe em que está sendo usado no desenvolvimento e que não tem nenhum tipo de retorno. Por exemplo:

public class Person {
 public Person() {
 }
}

Cada classe tem, no mínimo, um construtor, e se você não definir explicitamente um construtor para a sua classe, o compilador gerará um para você, o que é chamado de construtor padrão. A definição da classe precedente e desta são idênticas no modo como funcionam:

public class Person {
}

Chamando um Construtor de Superclasse

Para chamar um construtor de superclasse diferente do construtor padrão, é necessário fazê-lo explicitamente. Por exemplo, suponha que Person tenha um construtor que leva o nome do objeto Person que está sendo criado. No construtor padrão de Employee, é possível chamar o construtor Person mostrado na listagem 13:

Listagem 13. Inicializando um Novo Employee
public class Person {
 private String name;
 public Person() {
 }
 public Person(String name) {
   this.name = name;
 }
}

// Meanwhile, in Employee.java
public class Employee extends Person {
 public Employee() {
   super("Elmer J Fudd");
 }
}

Provavelmente, você nunca quererá inicializar um novo objeto Employee desse modo. Até ficar mais confortável com os conceitos orientados a objeto e com a sintaxe do Java em geral, é uma boa ideia implementar construtores de superclasse nas subclasses que você acredita que precisará delas, e chamá-las homogeneamente. A listagem 14 define um construtor em Employee que se parece com o presente em Person de modo que eles são correspondentes. É muito menos confuso do que um ponto de vista de manutenção.

Listagem 14. Chamando uma superclasse homogeneamente
public class Person {
 private String name;
 public Person(String name) {
   this.name = name;
 }
}
// Meanwhile, in Employee.java
public class Employee extends Person {
 public Employee(String name) {
   super(name);
 }
}

Declarando um Construtor

A primeira coisa que um construtor faz é chamar o construtor padrão da sua superclasse imediata, a não ser que você — na primeira linha de código do construtor — chame um construtor diferente. Por exemplo, estas duas declarações são funcionalmente idênticas, assim escolha uma:

public class Person {
 public Person() {
 }
}
// Meanwhile, in Employee.java
public class Employee extends Person {
 public Employee() {
 }
}

Ou:

public class Person {
 public Person() {
 }
}
// Meanwhile, in Employee.java
public class Employee extends Person {
 public Employee() {
   super();
 }
}

Construtores Sem Argumentos

Se você fornecer um construtor alternativo, deverá fornecer explicitamente o construtor padrão ou ele não estará disponível. Por exemplo, o código a seguir gerará um erro de compilação:

public class Person {
 private String name;
 public Person(String name) {
   this.name = name;
 }
}
// Meanwhile, in Employee.java
public class Employee extends Person {
 public Employee() {
 }
}

Este exemplo não tem nenhum construtor padrão, porque ele fornece um construtor alternativo sem incluir explicitamente o construtor padrão. É esse o motivo por que o construtor padrão, algumas vezes, é chamado de construtor sem argumentos (ou no-arg, em inglês); porque há condições em que ele não está incluído; ele não é realmente um padrão.

Como Construtores Chamam Construtores

Um construtor dentro de uma classe pode ser chamado por outro construtor usando a palavra-chave this, junto com uma lista de argumentos. Assim como super(), this() deve ser a primeira linha no construtor. Por exemplo:

public class Person {
 private String name;
 public Person() {
   this("Some reasonable default?");
 }
 public Person(String name) {
   this.name = name;
 }
}
// Meanwhile, in Employee.java

Você verá essa linguagem com frequência, quando um construtor delega para outro, passando algum valor padrão se esse construtor for chamado. É também um modo maior de incluir um novo construtor em uma classe enquanto minimiza o impacto no código que já usa um construtor mais antigo.

Níveis de Acesso do Construtor

Os construtores podem ter o nível de acesso que você quiser e se aplicam determinadas regras de visibilidade. A Tabela 1 resume as regras de acesso do construtor:

Tabela 1. Regras de Acesso do Construtor
Modificador de Acesso do Construtor Descrição
publicO construtor pode ser chamado por qualquer classe.
protectedO construtor pode ser chamado por uma classe no mesmo pacote ou em qualquer subclasse.
Nenhum modificador (privado do pacote)O construtor pode ser chamado por qualquer classe no mesmo pacote.
privateO construtor pode ser chamado apenas pela classe em que o construtor está definido.

Você pode ser capaz de pensar em casos de uso em que os construtores são declarados como protected ou privados do pacote, mas como um construtor private é útil? Eu usei construtores privados quando não queria permitir a criação direta de um objeto por meio da palavra-chave new durante a implementação, ou seja, o padrão Factory (veja Recursos ). Nesse caso, um método estático seria usado para criar instâncias da classe e esse método, sendo incluído na classe por si só, teria permissão de chamar o construtor privado:

Herança e Abstração

Se uma subclasse substituir um método de uma superclasse, o método estará essencialmente oculto porque a chamada desse método por meio de uma referência à subclasse chamará a versão do método da subclasse, não a versão da superclasse. Isso não significa que o método da superclasse não está mais acessível. A subclasse pode chamar o método da superclasse prefaciando o nome do método com a palavra-chave super (e, diferente das regras do construtor, isso pode ser feito em qualquer linha no método da subclasse, ou mesmo em um método completamente diferente). Por padrão, um programa Java chamará o método da subclasse se ele for chamado por uma referência à subclasse.

O mesmo se aplica às variáveis, desde que o responsável pela chamada tenha acesso à variável (ou seja, a variável esteja visível para o código que está tentando acessá-la). Isso pode não deixar de significar um sofrimento para você à medida que avançar na proficiência da programação do Java. O Eclipse fornecerá amplos avisos de que você está ocultando uma variável de uma superclasse ou que uma chamada de método não chamará o que se espera dela.

Em um contexto de OOP, abstração se refere à generalização dos dados e comportamento para um tipo mais elevado na hierarquia de herança do que a classe atual. Quando você move variáveis ou métodos de uma subclasse para uma superclasse, você diz que está abstraindo tais membros. A principal razão para fazer isso é a reutilização de código comum, empurrando-o pela hierarquia o máximo possível. Ter código comum em um lugar simplifica a manutenção.

Abstrair Classes e Métodos

Haverá ocasiões em que você desejará criar classes que sirvam apenas como abstrações e que não precisem necessariamente ser instanciadas. Essas classes são chamadas de classes abstratas. Da mesma forma, você encontrará momentos em que certos métodos precisam ser implementados de forma diferente para cada subclasse em relação a como são implementados para a superclasse. Tais métodos são métodos abstratos. Estas são algumas regras básicas sobre classes e métodos abstratos:

  • Qualquer classe pode ser declarada como abstract.
  • As classes abstratas não podem ser instanciadas.
  • Um método abstrato não pode conter um corpo de método.
  • Qualquer classe com um método abstrato pode ser declarada como abstract.

Usando a Abstração

Suponha que você deseja permitir que a classe Employee seja instanciada diretamente. Simplesmente, declare isso usando a palavra-chave abstract e pronto:

public abstract class Employee extends Person {
// etc.
}

Se você tentar executar esse código, obterá um erro de compilação:

public void someMethodSomwhere() {
 Employee p = new Employee();// compile error!!
}

O compilador está reclamando que Employee é abstrato e não pode ser instanciado.

O Poder da Abstração

Suponha que você precise de um método para examinar o estado de um objeto Employee e assegurar que ele seja válido. Essa necessidade parece ser comum a todos os objetos Employee, mas pode se comportar suficientemente diferente entre algumas subclasses potenciais cuja possibilidade de reutilização é zero. Nesse caso, você declara o método validate() como abstract (forçando todas as subclasses a implementá-lo):

public abstract class Employee extends Person {
 public abstract boolean validate();
}

Cada subclasse direta de Employee (por exemplo, Manager) agora precisará implementar o método validate(). No entanto, assim que a subclasse implementar o método validate(), nenhuma das subclasses precisará implementá-lo.

Por exemplo, suponha que você tenha um objeto Executive que estenda Manager. Essa definição seria perfeitamente válida:

public class Executive extends Manager {
 public Executive() {
 }
}

Quando (Não) Abstrair: Duas Regras

Como primeira regra geral, não abstraia no seu design inicial. Usar classes abstratas prematuramente no design o forçará a seguir por um caminho e isso poderá restringir seu aplicativo. Lembre-se de que sempre é possível refatorar o comportamento comum (que é o ponto de sustentação das classes abstratas) em um nível mais elevado no gráfico de herança. Quase sempre é melhor fazer isso depois que você descobrir que precisa fazê-lo. O Eclipse tem um suporte maravilhoso para a refatoração.

Segundo, por mais poderosas que sejam, resista usar classes abstratas o máximo que puder. A menos que as superclasses contenham lotes de comportamento comum e, em si mesmas, não tenham nenhum significado, deixe-as não abstratas. Gráficos de herança profundos podem tornar a manutenção do código difícil. Considere trocar classes que sejam muito grandes e tenham muito código para ser mantido.

Atribuições: Classes

É possível atribuir uma referência de uma classe para uma variável de um tipo que pertença a outra classe, mas existem regras. Vamos ver um exemplo:

Manager m = new Manager();
Employee e = new Employee();
Person p = m; // okay
p = e; // still okay
Employee e2 = e; // yep, okay
e = m; // still okay
e2 = p; // wrong!

A variável de destino deve ser de um supertipo de classe que pertence à referência de origem ou o compilador retornará um erro. Basicamente, tudo o que está do lado direito da atribuição deve ser um subclasse ou da mesma classe da coisa no lado esquerdo. Se não for, é possível que atribuições de objetos com diferentes gráficos de herança (por exemplo Manager e Employee) sejam atribuídos a uma variável do tipo errado. Considere este exemplo:

Manager m = new Manager();
Person p = m; // so far so good
Employee e = m; // okay
Employee e = p; // wrong!

Ao passo que Employee é um Person, ele definitivamente não é um Manager, e o compilador força isso.


Interfaces

Nesta seção, você começará a aprender sobre interfaces e a utilizá-las no código Java.

Definindo uma Interface

Uma interface é um conjunto nomeado de comportamentos (e/ou elementos de dados constantes) para o qual um implementador deve fornecer código. Uma interface especifica o comportamento que a implementação oferece, mas não como ela é realizada.

A definição de uma interface é simples:

public interface interfaceName {
 returnType methodName( argumentList );
}

Uma declaração de interface se parece com uma declaração de classe, exceto que você usa a palavra-chave interface. Você pode atribuir à interface o nome que desejar (sujeito às regras da linguagem), no entanto, por convenção, os nomes de interface se parecem com os nomes de classe.

Os métodos definidos em uma interface não têm nenhum corpo de método. O implementador da interface é responsável por fornecer o corpo do método (assim como com os métodos abstratos).

Você define as hierarquias de interfaces do mesmo modo que para as classes, exceto que uma única classe pode implementar tantas interfaces quanto for necessário. (Lembre-se, uma classe pode estender apenas uma classe.) Se uma classe estender outra classe e implementar interfaces, estas serão listadas depois da classe estendida, como segue:

public class Manager extends Employee implements BonusEligible, StockOptionRecipient {
// Etc...
}

Interfaces de Marcador

Afinal, uma interface não precisa ter nenhum corpo. Na verdade, a definição a seguir é perfeitamente aceitável:

public interface BonusEligible {
}

Falando em termos gerais, essas interfaces são chamadas de interfaces de marcador, porque elas marcam uma classe como implementação da interface , mas não oferecem nenhum comportamento explícito.

Depois de conhecer tudo isso, na verdade, a definição de uma interface é fácil:

public interface StockOptionRecipient {
 void processStockOptions(int numberOfOptions, BigDecimal price);
}

Implementando Interfaces

Para usar uma interface, basta implementá-la, o que significa simplesmente fornecer um corpo de método, o qual, por sua vez, fornecerá o comportamento que cumpre o contrato da interface. Você faz isso com a palavra-chave implements:

public class className extends superclassName implementsinterfaceName {
// Class Body
}

Suponha que você implemente a interface StockOptionRecipient na classe Manager, como mostrado na listagem 15:

Listagem 15. Implementando uma Interface
public class Manager extends Employee implements StockOptionRecipient {
 public Manager() {
 }
 public void processStockOptions (int numberOfOptions, BigDecimal price) {
   log.info("I can't believe I got " + number + " options at $" +
            price.toPlainString() + "!");  }
}

Ao implementar a interface, você fornece o comportamento dos métodos na interface. É necessário implementar os métodos com assinatura que correspondam aos presentes na interface, com a adição do modificador de acesso public.

Gerando Interfaces no Eclipse

O Eclipse poderá facilmente gerar a assinatura de método correta se você decidir que uma das classes deverá implementar uma interface. Apenas mude a assinatura da classe para implementar a interface. O Eclipse colocará uma linha vermelha ondulada sob a classe, sinalizando-a como erro porque a classe não fornece os métodos na interface. Clique no nome da classe com o mouse, pressione Ctrl + 1 e o Eclipse sugerirá "correções rápidas" para você. Das correções sugeridas, escolha Add Unimplemented Methods e o Eclipse gerará os métodos, colocando-os na parte inferior do arquivo de origem.

Uma classe abstrata pode declarar que ela implementa uma interface específica, mas ela não é necessária para implementar todos os métodos na interface. Isso se dá porque as classes abstratas não são obrigatórias para fornecer implementações para todos os métodos que precisam ser implementados. No entanto, a primeira classe concreta (ou seja, a primeira que pode ser instanciada) deverá implementar todos os métodos que a hierarquia não implementou.

Usando Interfaces

Uma interface define um novo tipo de dados de referência, o que significa que será possível fazer referência a uma interface sempre que fizer referência a uma classe. Isso inclui quando você declara uma variável de referência ou faz um cast de um tipo para outro, como mostrado na listagem 16.

Listagem 16. Atribuindo uma nova instância Manager para uma referência a StockOptionEligible
public static void main(String[] args) {
 StockOptionEligible soe = new Manager();// perfectly valid
 calculateAndAwardStockOptions(soe);
 calculateAndAwardStockOptions(new Manager());// works too
}
. . .
public static void calculateAndAwardStockOptions(StockOptionEligible soe) {
 BigDecimal reallyCheapPrice = BigDecimal.valueOf(0.01);
 int numberOfOptions = 10000;
 soe.processStockOptions(numberOfOptions, reallyCheapPrice);
}

Como você pode ver, é perfeitamente válido atribuir uma nova instância Manager para uma referência a StockOptionEligible, além de informar uma nova instância Manager a um método que espera uma referência a StockOptionEligible.

Atribuições: Classes

É possível atribuir uma referência de uma classe que implementa uma interface para uma variável de um tipo de interface, mas existem regras. Na listagem 16, nós vemos que a atribuição da instância Manager para uma referência à variável StockOptionEligible é perfeitamente válida. O motivo é que a classe Manager implementa essa interface. No entanto, a seguinte atribuição não é válida:

  Manager m = new Manager();
  StockOptionEligible soe = m; //okay
  Employee e = soe; // Wrong!

Como Employee é supertipo de Manager, pode parecer à primeira vista que está tudo bem, mas não está. Como Manager é uma especialização de Employee, é *diferente* e, neste caso em especial, implementa uma interface que Employee não implementa.

Atribuições como essas seguem as regras de atribuição que vimos em Herança. E assim como acontece com as classes, é possível atribuir apenas uma referência à interface para uma variável do mesmo tipo ou para um tipo de superinterface.


Classes Aninhadas

Nesta seção, aprenda sobre classes aninhadas, onde e como usá-las.

Onde Usar Classes Aninhadas

Como o nome sugere, uma classe aninhada é aquela dentro de outra classe. Esta é uma classe aninhada:

public class EnclosingClass {
. . .
 public class NestedClass {
 . . .

 }
}

Assim como as variáveis e métodos do membro, as classes Java também podem ser definidas em qualquer escopo incluindo public, private ou protected. As classes aninhadas podem ser úteis quando você quer manipular o processamento interno dentro da sua classe de um modo orientado a objeto, mas essa funcionalidade é limitada para a classe que precisa dela.

Normalmente, você usará uma classe aninhada nos casos em que precisa de uma classe que esteja intimamente ligada à classe em que ela está definida. Uma classe aninhada tem acesso aos dados privados dentro da classe que a encerra, mas isso ocorre com alguns efeitos colaterais que não são óbvios quando você começa a trabalhar com classes aninhadas (ou internas).

O Escopo nas Classes Aninhadas

Como a classe aninhada tem um escopo, ela está limitada pelas regras do escopo. Por exemplo, uma variável de membro pode ser acessada apenas por uma instância da classe (um objeto). O mesmo é verdade para uma classe aninhada.

Suponha que você tenha o seguinte relacionamento entre um Manager e uma classe aninhada denominada DirectReports, que é uma coleção dos Employees que se reportam a esse Manager:

public class Manager extends Employee {
 private DirectReports directReports;
 public Manager() {
   this.directReports = new DirectReports();
 }
. . .
 private class DirectReports {
 . . .
 }
}

Assim como cada objeto Manager representa um único ser humano, o objeto DirectReports representa uma coleção de pessoas reais (funcionários) que se reportam a um gerente (Manager). O DirectReports será diferente de um Manager para outro. Nesse caso, faz sentido que ele faça referência à classe aninhada DirectReports no contexto da instância de Manager que a encerra, assim ela foi feita private.

Classes Aninhadas Públicas

Como é private, apenas Manager pode criar uma instância de DirectReports. Mas, suponha que você queira atribuir a uma entidade externa a capacidade de criar instâncias de DirectReports? Nesse caso, seria possível atribuir à classe DirectReports um escopo public e, desse modo, qualquer código externo poderia criar instâncias de DirectReports, como mostrado na listagem 17:

Listagem 17. Criando instâncias de DirectReports: primeira tentativa
public class Manager extends Employee {
 public Manager() {
 }
. . .
 private class DirectReports {
 . . .
 }
}
//
public static void main(String[] args) {
 Manager.DirectReports dr = new Manager.DirectReports();// This won't work!
}

O código na listagem 17 não funciona, e você provavelmente está se perguntando o porquê. O problema (e também sua solução) está no modo como DirectReports está definido dentro de Manager, e nas regras do escopo.

As Regras do Escopo, Revistas

Se você tinha uma variável de membro Manager, esperava que o compilador solicitasse que você fizesse uma referência a um objeto Manager antes que fosse possível fazer a referência, certo? Bem o mesmo se aplica a DirectReports, pelo menos como você o definiu na listagem 17.

Para criar uma instância de uma classe aninhada pública, você usa uma versão especial do operador new. Combinado com uma referência a alguma instância encerrada em uma classe externa, new permite que você crie uma instância da classe aninhada:

public class Manager extends Employee {
 public Manager() {
 }
. . .
 private class DirectReports {
 . . .
 }
}
// Meanwhile, in another method somewhere...
public static void main(String[] args) {
 Manager manager = new Manager();
 Manager.DirectReports dr = manager.new DirectReports();
}

Observe que a sintaxe chama por uma referência para uma instância de fechamento, mais um ponto e a palavra new, seguido pela classe que você deseja criar.

Classes Internas Estáticas

Sempre que você deseja criar uma classe que está intimamente ligada (conceptualmente) a uma classe, mas as regras de escopo são flexíveis e não exigem uma referência a uma instância de fechamento. É onde entram as classes internas estáticas. Um exemplo comum disso é implementar um Comparator, que é usado para comparar duas instâncias da mesma classe, normalmente com o propósito de ordenar (ou classificar) as classes:

public class Manager extends Employee {
. . .
 public static class ManagerComparator implements Comparator<Manager> {
   . . .
 }
}
// Meanwhile, in another method somewhere...
public static void main(String[] args) {
 Manager.ManagerComparator mc = new Manager.ManagerComparator();
 . . .
}

Nesse caso, não é necessária uma instância de fechamento. As classes internas estáticas atuam como suas contrapartes, as classes regulares Java, e só devem ser realmente usadas quando é necessário ligar intimamente uma classe e sua definição. Claramente, no caso de uma classe de utilitário como o ManagerComparator, criar uma classe externa é desnecessário e pode encher desordenadamente sua base de código. Definir essas classes como classes internas estáticas é o caminho a seguir.

Classes Internas Anônimas

A linguagem Java permite que você declare classes praticamente em qualquer lugar, inclusive no meio de um método, se necessário, e mesmo sem atribuir um nome para ela. Basicamente, isso é um truque do compilador, mas há momentos em que ter classes internas anônimas é extremamente prático.

A listagem 18 baseia-se no exemplo da listagem 15, incluindo um método padrão para manipulação dos tipos de Employee que não sejam StockOptionEligible:

Listagem 18. Manipulando tipos de Employee que não são StockOptionEligible
public static void main(String[] args) {
 Employee employee = new Manager();// perfectly valid
 handleStockOptions(employee);
 employee = new Employee();// not StockOptionEligible
 handleStockOptions(employee);
}
. . .
private static void handleStockOptions(Employee e) {
 if (e instanceof StockOptionEligible) {
   calculateAndAwardStockOptions((StockOptionEligible)e);
 } else {
   calculateAndAwardStockOptions(new StockOptionEligible() {
     @Override
     public void awardStockOptions(int number, BigDecimal price) {
       log.info("Sorry, you're not StockOptionEligible!");
     }
   });
 }
}
. . .
private static void calculateAndAwardStockOptions(StockOptionEligible soe) {
 BigDecimal reallyCheapPrice = BigDecimal.valueOf(0.01);
 int numberOfOptions = 10000;
 soe.processStockOptions(numberOfOptions, reallyCheapPrice);

}

Nesse exemplo, você fornece uma implementação da interface StockOptionEligible usando uma classe interna anônima para as instâncias de Employee que não implementam essa interface. As classes internas anônimas também são úteis para implementar métodos de retorno e na manipulação de eventos.


Expressões Regulares

Uma expressão regular é essencialmente um padrão que descreve um conjunto de sequências que compartilham tal padrão. Se você for programador de Perl, deverá se sentir em casa com a sintaxe padrão da expressão regular (regex) na linguagem Java. Se você não está acostumado à sintaxe das expressões regulares, elas podem parecer estranhas. Esta seção é uma introdução ao uso de expressões regulares nos seus programas Java.

A Regular Expressions API

Este é um conjunto de sequências que têm alguma coisa em comum:

  • A string
  • A longer string
  • A much longer string

Observe que cada uma dessas cadeias de caractere começa com a e terminam com string. A Java Regular Expressions API (veja Recursos ) é útil para extrair esses elementos, ver o padrão entre eles e fazer coisas interessantes com as informações recolhidas.

A Regular Expressions API tem três classes principais que você usará praticamente o tempo todo:

  • Pattern que descreve um padrão de cadeia de caractere.
  • Matcher que testa uma cadeia de caractere para ver se ela corresponde ao padrão.
  • PatternSyntaxException que informa algo não aceitável sobre o padrão que você tentou definir.

Você começará a trabalhar com padrões simples de expressões regulares que usam essas classes em breve. Antes de fazer isso, no entanto, dê uma olhada na sintaxe padrão da regex.

Sintaxe Padrão da Regex

Um padrão de regex descreve a estrutura da cadeia de caractere que a expressão tentará localizar em uma sequência de saída. É aí que expressões regulares podem parecer um pouco estranhas. Assim que você compreender a sintaxe, no entanto, elas se tornam fáceis de decifrar. A tabela 2 lista alguns dos desenvolvimentos de regex mais comuns que você usará nas sequências do padrão:

Tabela 2. Desenvolvimentos de regex comuns
Desenvolvimento de regexO que qualifica como uma correspondência
.Qualquer caractere
?Zero (0) ou um (1) do que veio antes
*Zero (0) ou mais do que veio antes
+Um (1) ou mais do que veio antes
[]Um intervalo de caracteres ou dígitos
^A negação do que se segue (ou seja, "não o que quer que seja")
\dQualquer dígito (alternativamente, [0-9])
\DQualquer não dígito (alternativamente, [^0-9])
\sQualquer caractere espaço em branco (alternativamente, [\n\t\f\r])
\SQualquer caractere não espaço em branco (alternativamente, [^\n\t\f\r])
\wQualquer caractere de palavra (alternativamente, [a-zA-Z_0-9])
\WQualquer caractere de não palavra (alternativamente, [^\w])

Os primeiros desenvolvimentos são chamados de quantificadores, porque quantificam o que vem antes deles. Desenvolvimentos como \d são classes de caractere predefinidas. Qualquer caractere que não tenha nenhum significado especial em um padrão é um literal e corresponde a si mesmo.

Reconhecimento de padrões

Armado com a sintaxe padrão da tabela 2, é possível trabalhar no exemplo simples da listagem 19, usando as classes na Java Regular Expressions API:

Listagem 19. Reconhecimento de padrões com regex
Pattern pattern = Pattern.compile("a.*string");
Matcher matcher = pattern.matcher("a string");
boolean didMatch = matcher.matches();
Logger.getAnonymousLogger().info (didMatch);
int patternStartIndex = matcher.start();
Logger.getAnonymousLogger().info (patternStartIndex);
int patternEndIndex = matcher.end();
Logger.getAnonymousLogger().info (patternEndIndex);

Primeiro, a listagem 19 cria uma classe Pattern chamando compile(), que é um método estático no Pattern, com uma cadeia de caractere literal que se deseja corresponder. Esse literal usa a sintaxe padrão da regex. Neste exemplo, a tradução para o inglês do padrão é:

Find a string of the form a followed by zero or more characters, followed by string.

Métodos de Correspondência

Em seguida, a listagem 19 chama o matcher() no Pattern. Essa chamada cria uma instância do Matcher. Quando isso acontece, o Matcher pesquisa a cadeia de caractere que você passou para as correspondências em relação à cadeia de caractere padrão usada quando você criou o Pattern.

Cada cadeia de caractere da linguagem Java é uma coleção indexada de caracteres, começando com 0 e terminando com a extensão da sequência menos um. O Matcher analisa a cadeia de caractere, iniciando em 0 e procura correspondências nela. Depois que o processo é concluído, o Matcher contém informações sobre as correspondências encontradas (ou não encontradas) na cadeia de caractere de entrada. É possível acessar essas informações chamando vários métodos no Matcher:

  • matches() informa se a sequência de entrada inteira era uma correspondência exata do padrão.
  • start() informa o valor do índice na sequência onde a cadeia de caractere correspondida inicia.
  • end() informa o valor do índice na cadeia de caractere onde a cadeia de caractere correspondida termina, mais um.

A listagem 19 localiza uma única correspondência iniciando em 0 e terminando em 7. Assim, a chamada para matches() retorna true, a chamada para start() retorna 0 e a chamada para end() retorna 8 .

lookingAt() versus matches()

Se houver mais caracteres na cadeia de caractere do que no padrão pesquisado, uma opção é usar lookingAt() em vez de matches(). lookingAt() procura por padrões de subsequência que correspondem a determinado padrão. Por exemplo, considere a seguinte cadeia de caractere:

Here is a string with more than just the pattern.

É possível procurar por a.*string e obter uma correspondência se usar lookingAt(). Mas se você usar matches(), ele retornará false, porque há mais na cadeia de caractere do que há apenas no padrão.

Padrões Complexos na Regex

Procuras simples são fáceis de fazer com as classes regex, mas também é possível fazer coisas altamente sofisticadas com a Regular Expressions API.

Umwiki, como você certamente sabe, é um sistema baseado na Web que permite que os usuários modifiquem as páginas. Os wikis se baseiam quase que inteiramente em expressões regulares. Seu conteúdo é baseado na entrada de cadeias de caractere dos usuários, que é analisada e formatada usando expressões regulares. Qualquer usuário pode criar um link para outro tópico em um wiki inserindo uma palavra wiki, que é normalmente uma série de palavras concatenadas, cada uma iniciando com uma letra maiúscula, por exemplo:

MyWikiWord

Com isso em mente sobre wikis, suponha a seguinte cadeia de caractere

Here is a WikiWord followed by AnotherWikiWord, then YetAnotherWikiWord.

Você pode procurar por palavras wiki nessa cadeia de caractere com um padrão de regex como este:

[A-Z][a-z]*([A-Z][a-z]*)+

E este é algum código para procurar por palavras wiki:

String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
 Logger.getAnonymousLogger().info("Found this wiki word: " + matcher.group());
}

Execute esse código e você verá as três palavras wiki no seu console.

Substituindo Sequências

A procura por correspondências é útil, mas também é possível manipular sequências assim que uma correspondência é encontrada. Isso pode ser feito substituindo as sequências correspondidas por alguma coisa, assim como você procura texto em um programa de processamento de texto e o substitui por outro texto. O Matcher tem alguns métodos de substituição de elementos de cadeia de caractere:

  • replaceAll() substitui todas as correspondências por uma cadeia de caractere específica.
  • replaceFirst() substitui apenas a primeira correspondência pela cadeia de caractere especificada.

Usar os métodos replace de Matcher é simples:

String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
Logger.getAnonymousLogger().info("Before: " + input);
String result = matcher.replaceAll("replacement");
Logger.getAnonymousLogger().info("After: " + result);

Esse código localiza palavras wiki, como antes. Quando o Matcher localiza uma correspondência, ele substitui o texto da palavra wiki por "replacement". Quando você executar esse código, deverá ver a seguinte saída no seu console:

Antes: Here is WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Depois: Here is replacement followed by replacement, then replacement.

Ser você usar replaceFirst(), deverá ver:

Antes: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Depois: Here is a replacement followed by AnotherWikiWord, then SomeWikiWord.

Correspondendo e Manipulando Grupos

Ao procurar por correspondências em um padrão de regex, é possível obter informações sobre o que é encontrado. Você já viu algo parecido com os métodos start() e end() no Matcher. Mas, também é possível fazer referência às correspondências por capturar grupos.

Em cada padrão, normalmente você cria grupos encerrando as partes do padrão entre parênteses. Os grupos são numerados da esquerda para a direita, iniciando com 1 (o grupo 0 representa a correspondência inteira). O código na listagem 20 substitui cada palavra wiki por uma cadeia de caractere que "envolve" a palavra:

Listagem 20. Correspondendo grupos
String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
Logger.getAnonymousLogger().info("Before: " + input);
String result = matcher.replaceAll("blah$0blah");
Logger.getAnonymousLogger().info("After: " + result);

Execute esse código e você obterá a seguinte saída do console:

Antes: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Depois: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
then blahSomeWikiWordblah.

Outra Abordagem de Correspondência de Grupos

A listagem 20 faz referência a toda correspondência incluindo $0 na cadeia de caractere de substituição. Qualquer parte de uma cadeia de caractere de substituição na forma $int se refere ao grupo identificado pelo número inteiro (assim $1 se refere ao grupo 1, e assim por diante). Em outras palavras, $0 é equivalente ao matcher.group(0);.

Você pode obter o mesmo objetivo de substituição usando alguns outros métodos. Em vez de chamar replaceAll(), é possível fazer o que segue:

StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
 matcher.appendReplacement(buffer, "blah$0blah");
}
matcher.appendTail(buffer);
Logger.getAnonymousLogger().info("After: " + buffer.toString());

E obter o mesmo resultado:

Antes: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Depois: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
then blahSomeWikiWordblah.

Genéricos

A introdução dos genéricos no JDK 5 marcou um grande salto para frente da linguagem Java. Se você usou os modelos do C++, achará que os genéricos na linguagem Java são similares, mas não exatamente idênticos. Se você não usou modelos do C++, não se preocupe: esta seção oferece uma introdução de alto nível aos genéricos na linguagem Java.

O Que São os Genéricos?

Com o release do JDK 5, repentinamente brotou da linguagem Java uma nova sintaxe, estranha e excitante. Basicamente, algumas classes JDK familiares foram substituídas por seus equivalentes genéricos.

Os genéricos são um mecanismo do compilador pelo qual é possível criar (e usar) tipos de coisas (como classes ou interfaces) de um modo genérico colhendo o código comum e parametrizando (ou modelando) o resto.

Os Genéricos em Ação

Para ver a diferença que os genéricos fazem, considere o exemplo de uma classe que está no JDK por um longo tempo: java.util.ArrayList, que é uma lista (List) de objetos (Object) suportada por um array.

A listagem 21 mostra como a java.util.ArrayList é instanciada:

Listagem 21. Instanciando ArrayList
ArrayList arrayList = new ArrayList();
arrayList.add("A String");
arrayList.add(new Integer(10));
arrayList.add("Another String");
// So far, so good.

Como é possível ver, a ArrayList é heterogênea: ela contém dois tipos String e um tipo Integer. Antes do JDK 5, não havia nada na linguagem Java para restringir esse comportamento, o que causava muitos erros de codificação. Na listagem 21, por exemplo,tudo parece bem até agora. Mas o que acontece ao acessar os elementos da ArrayList, o que a listagem 22 tenta fazer?

Listagem 22. Uma tentativa para acessar os elementos em ArrayList
ArrayList arrayList = new ArrayList();
arrayList.add("A String");
arrayList.add(new Integer(10));
arrayList.add("Another String");
// So far, so good.
*processArrayList(arrayList);
*// In some later part of the code...
private void processArrayList(ArrayList theList) {
   for (int aa = 0; aa < theList.size(); aa++) {
     // At some point, this will fail...
     String s = (String)theList.get(aa);
   }
}

Sem o conhecimento anterior do que está na ArrayList, você deve verificar o elemento que deseja acessar para ver se é possível manipular o seu tipo ou encarar uma possível ClassCastException.

Com os genéricos, é possível especificar o tipo de item que estava na ArrayList. A listagem 23 mostra como:

Listagem 23. Uma segunda tentativa, usando os genéricos
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("A String");
arrayList.add(new Integer(10));// compiler error!
arrayList.add("Another String");
// So far, so good.
*processArrayList(arrayList);
*// In some later part of the code...
private void processArrayList(ArrayList<String> theList) {
   for (int aa = 0; aa < theList.size(); aa++) {
     // No cast necessary...
     String s = theList.get(aa);
   }
}

Iterando com os Genéricos

Os genéricos melhoram a linguagem Java com a sintaxe especial para o tratamento de entidades como Lists que você normalmente quer percorrer elemento por elemento. Se você quiser processar a ArrayList, por exemplo, poderá reescrever o código da listagem 23 como segue:

private void processArrayList(ArrayList<String> theList) {
 for (String s : theList) {
   String s = theList.get(aa);
 }
}

Essa sintaxe funciona para qualquer tipo de objeto que seja Iterable (ou seja, que implemente a interface Iterable).

Classes Parametrizadas

As classes parametrizadas realmente brilham quando se trata de coleções; é desse modo que você vai olhar para elas. Considere a interface List (real). Ela representa uma coleção ordenada de objetos. No caso de uso mais comum, você inclui itens na List e os acessa pelo índice ou pelo processamento da List.

Se você está pensando sobre a parametrização de uma classe, considere se os seguintes critérios se aplicam:

  • Uma classe principal está no centro de algum tipo de wrapper: ou seja, a "coisa" no centro da classe pode ser aplicada amplamente, e os recursos (atributos, por exemplo) circundantes são idênticos.
  • Comportamento comum: você faz praticamente as mesmas operações, independentemente da "coisa" no centro da classe.

Aplicando esses dois critérios, é óbvio que a coleção se encaixa no projeto:

  • A "coisa" na classe que compreende a coleção.
  • As operações (como add, remove, size e clear) são praticamente as mesmas, independentemente do objeto que compreende a coleção.

Uma List Parametrizada

Na sintaxe dos genéricos, o código para criar uma List se parece como esta:

List<E> listReference = new concreteListClass<E>();

O E, que significa Element, é a "coisa" mencionada anteriormente. A concreteListClass é a classe do JDK que você está instanciando. O JDK inclui várias implementações List<E>, mas você usará ArrayList<E>. Outra forma possível para ver uma classe genérica discutida é Class<T>, onde T significa Type. Quando você vê E no código Java, normalmente isso se refere a uma coleção de algum tipo. E quando você vê T, isso denota uma classe parametrizada.

Assim, para criar uma ArrayList de, digamos, java.lang.Integer, faça o que segue:

List<Integer> listOfIntegers = new ArrayList<Integer>();

SimpleList: Uma Classe Parametrizada

Agora, suponha que você queira criar sua própria classe parametrizada denominada SimpleList, com três métodos:

  • add() inclui um elemento no final da SimpleList.
  • size() retorna o número de elementos na SimpleList.
  • clear() limpa completamente o conteúdo da SimpleList.

A listagem 24 mostra a sintaxe para parametrizar a SimpleList:

Listagem 24. Parametrizando a SimpleList
package com.makotogroup.intro;
import java.util.ArrayList;
import java.util.List;
public class SimpleList<E> {
 private List<E> backingStore;
 public SimpleList() {
   backingStore = new ArrayList<E>();
 }
 public E add(E e) {
   if (backingStore.add(e))
     return e;
   else
     return null;
 }
 public int size() {
   return backingStore.size();
 }
 public void clear() {
   backingStore.clear();
 }
}

A SimpleList pode ser parametrizada com qualquer subclasse Object. Para criar e usar uma SimpleList dos, digamos, objetos java.math.BigDecimal, faça o que segue:

public static void main(String[] args) {
 SimpleList<BigDecimal> sl = new SimpleList<BigDecimal>();
 sl.add(BigDecimal.ONE);
 log.info("SimpleList size is : " + sl.size());
 sl.add(BigDecimal.ZERO);
 log.info("SimpleList size is : " + sl.size());
 sl.clear();
 log.info("SimpleList size is : " + sl.size());
}

E a saída será:

May 5, 2010 6:28:58 PM com.makotogroup.intro.Application main
INFO: SimpleList size is : 1
May 5, 2010 6:28:58 PM com.makotogroup.intro.Application main
INFO: SimpleList size is : 2
May 5, 2010 6:28:58 PM com.makotogroup.intro.Application main
INFO: SimpleList size is : 0

Tipos Enum

No JDK 5, foi incluído um novo tipo de dados na linguagem Java, denominado enum. Não confundir com o java.util.Enumeration; o enum representa um conjunto de objetos constantes todos eles relacionados a determinado conceito, cada qual representando um valor constante diferente no conjunto. Antes do enum ser apresentado na linguagem Java, você tinha de definir um conjunto de valores constantes para um conceito (digamos, gênero), por exemplo:

public class Person {
 public static final String MALE = "male";
 public static final String FEMALE = "female";
}

Seja qual fosse o código necessário para referência, esse valor constante tinha de ser escrito como o que segue:

public void myMethod() {
 //. . .
 String genderMale = Person.MALE;
 //. . .
}

Definindo Constantes com o enum

Usar o tipo enum torna a definição de constantes muito mais formal, e também mais potente. Esta é a definição do enum para Gender:

public enum Gender {
 MALE,
 FEMALE
}

Isso apenas arranha a superfície do que os enums podem fazer. Na verdade, os enums são parecidos com as classes, assim eles podem ter construtores, atributos e métodos:

package com.makotogroup.intro;

public enum Gender {
 MALE("male"),
 FEMALE("female");

 private String displayName;
 private Gender(String displayName) {
   this.displayName = displayName;
 }

 public String getDisplayName() {
   return this.displayName;
 }
}

Uma diferença entre uma classe e um enum é que o construtor do enum deve ser declarado como private, e não pode estender (nem ser herdado de) outros enums. No entanto, um enum pode implementar uma interface.

Um enum Implementa uma Interface

Suponha que você defina uma interface, Displayable:

package com.makotogroup.intro;
public interface Displayable {
 public String getDisplayName();
}

Seu enum Gender pode implementar essa interface (assim como qualquer outro enum que seja necessário para produzir um nome de exibição amigável), por exemplo:

package com.makotogroup.intro;

public enum Gender implements Displayable {
 MALE("male"),
 FEMALE("female");

 private String displayName;
 private Gender(String displayName) {
   this.displayName = displayName;
 }
 @Override
 public String getDisplayName() {
   return this.displayName;
 }
}

Consulte Recursos para obter mais informações sobre os genéricos.


E/S

Esta seção é uma visão geral do pacote java.io .Você aprenderá a usar algumas ferramentas para coletar e manipular dados de uma variedade de origens.

Trabalhando com Dados Externos

Na maior parte das vezes, os dados que você usa em seus programas Java virão de uma origem de dados externa, como um banco de dados, a transferência direta de bytes por um soquete ou o armazenamento de arquivo. A linguagem Java oferece muitas ferramentas para obter informações dessas origens e a maioria delas está localizada no pacote java.io .

Files

De todas as origens de dados disponíveis nos seus aplicativos Java, os arquivos são os mais comuns e, frequentemente, os mais convenientes. Se você deseja ler um arquivo no seu aplicativo Java, deverá usar streams que analisa os bytes de entrada nos tipos da linguagem Java.

O java.io.File é uma classe que define um recurso no seu sistema de arquivos e representa esse recurso de um modo abstrato. A criação de um objeto File é fácil:

File f = new File("temp.txt");
File f2 = new File("/home/steve/testFile.txt");

O construtor File assume o nome do arquivo que será criado. A primeira chamada cria um arquivo denominado temp.txt no diretório especificado. A segunda chamada cria um arquivo em um local específico no sistema Linux. É possível passar qualquer String para o construtor File, desde que seja um nome de arquivo válido para o seu SO, quer o arquivo de referência exista, quer não.

Este código pergunta ao objeto File recém-criado se o arquivo existe:

File f2 = new File("/home/steve/testFile.txt");
if (f2.exists()) {
 // File exists. Process it...
} else {
 // File doesn't exist. Create it...
 f2.createNewFile();
}

O java.io.File apresenta alguns outros métodos práticos que você pode usar para excluir arquivos; criar diretórios (passando um nome de diretório como argumento para o construtor de File); determinar se um recurso é arquivo, diretório ou link simbólico; assim por diante.

A real ação do Java I/O é gravar e ler dados em origens de dados, e é aí que entra os fluxos.

Usando Fluxos no Java I/O

É possível acessar arquivos no sistema de arquivos usando fluxos. No nível mais baixo, os fluxos permitem que um programa receba bytes de uma origem ou enviem uma saída para um destino. Alguns fluxos manipulam todos os tipos de caracteres de 16 bits (tipos Reader e Writer). Outros manipulam apenas bytes de 8 bits(tipos InputStream e OutputStream). Dentro dessas hierarquias, há uma grande variedade de fluxos, todos encontrados no pacote java.io. No nível mais alto da abstração estão os fluxos de caracteres e os fluxos de bytes.

Os fluxos de bytes leem (InputStream e subclasses) e gravam (OutputStream e subclasse) bytes de 8 bits. Em outras palavras, um fluxo de bytes pode ser considerado um tipo de fluxo mais bruto. Este é um resumo de dois fluxos de bytes comuns e de sua utilização:

  • FileInputStream/FileOutputStream: lê bytes de um arquivo, grava bytes em um arquivo.
  • ByteArrayInputStream/ByteArrayOutputStream: lê bytes em um array na memória, grava bytes em um array na memória.

Fluxos de Caracteres

Os fluxos de caracteres leem (Reader e suas subclasses) e gravam (Writer e suas subclasses) caracteres de 16 bits. Esta é uma listagem selecionada de fluxos de caracteres e sua utilização:

  • StringReader/StringWriter: lê e grava caracteres em uma Strings na memória.
  • InputStreamReader/InputStreamWriter (e subclasses FileReader/FileWriter): formam uma ponte entre fluxos de bytes e fluxos de caracteres. A variedade de Reader lê bytes em um fluxo de bytes e os converte em caracteres. A variedade de Writer converte caracteres em bytes para inseri-los nos fluxos de bytes.
  • BufferedReader/BufferedWriter: armazena dados em buffer durante a leitura ou gravação de outro fluxo, tornando as operações de leitura e gravação mais eficientes.

Em vez de tentar abordar totalmente os fluxos, enfocarei os recomendados para leitura e gravação de arquivos. Na maioria dos casos, esses são os fluxos de caracteres.

Lendo um File

Há vários meios para se ler um File. Provavelmente, a abordagem mais simples é:

  1. Criar um InputStreamReader no File que você deseja ler.
  2. Chame read() para ler um caractere por vez até chegar ao final do arquivo.

A listagem 25 é um exemplo de leitura de um File:

Listagem 25. Lendo um File
Logger log = Logger.getAnonymousLogger();
StringBuilder sb = new StringBuilder();
try {
 InputStream inputStream = new FileInputStream(new File("input.txt"));
 InputStreamReader reader = new InputStreamReader(inputStream);
 try {
   int c = reader.read();
   while (c != -1) {
     sb.append(c);
   }
 } finally {
 reader.close();
 }
} catch (IOException e) {
 log.info("Caught exception while processing file: " + e.getMessage());
}

Gravando em um File

Como com a leitura de um File, há vários modos para se gravar em um Arquivo. Novamente, tomarei a abordagem mais simples:

  1. Crie um FileOutputStream no File em que você deseja gravar.
  2. Chame write() para gravar a sequência de caracteres.

A listagem 26 é um exemplo de gravação em um File:

Listagem 26. Gravando em um File
Logger log = Logger.getAnonymousLogger();
StringBuilder sb = getStringToWriteSomehow();
try {
 OutputStream outputStream = new FileOutputStream(new File("output.txt"));
 OutputStreamWriter writer = new OutputStreamWriter(outputStream);
 try {
   writer.write(sb.toString());
 } finally {
   writer.close();
 }
} catch (IOException e) {
 log.info("Caught exception while processing file: " + e.getMessage());
}

Armazenando Fluxos em Buffer

A leitura ou gravação de fluxos de caracteres um caractere por vez não é exatamente eficiente, assim, na maioria dos casos, você provavelmente preferirá usar E/S armazenada em buffer. Para ler um arquivo usando E/S armazenada em buffer, o código se parecerá com o da listagem 25, a não ser que você encapsule o InputStreamReader em umBufferedReader, como mostrado na listagem 27:

Listagem 27. Lendo um File com E/S armazenada em buffer
Logger log = Logger.getAnonymousLogger();
StringBuilder sb = new StringBuilder();
try {
 InputStream inputStream = new FileInputStream(new File("input.txt"));
 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
 try {
   String line = reader.readLine();
   while (line != null) {
     sb.append(line);
     line = reader.readLine();
   }
 } finally {
 reader.close();
 }
} catch (IOException e) {
 log.info("Caught exception while processing file: " + e.getMessage());
}

Gravar em um arquivo usando E/S armazenada em buffer é a mesma coisa: simplesmente, encapsule o OutputStreamWriter em um BufferedWriter, como mostrado na listagem 28

Listagem 28. Gravando em um File com E/S armazenada em buffer
Logger log = Logger.getAnonymousLogger();
StringBuilder sb = getStringToWriteSomehow();
try {
 OutputStream outputStream = new FileOutputStream(new File("output.txt"));
 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
 try {
   writer.write(sb.toString());
 } finally {
   writer.close();
 }
} catch (IOException e) {
 log.info("Caught exception while processing file: " + e.getMessage());
}

Eu simplesmente arranhei a superfície do que é possível fazer com a biblioteca básica do Java. Por conta própria, tente aplicar o que você aprendeu sobre arquivos em outras origens de dados.


Serialização do Java

As serialização do Java é outra das bibliotecas básicas da plataforma Java. A serialização é primeiramente usada para a persistência e composição remota de objetos, dois casos de uso em que é necessário ser capaz de tirar uma captura instantânea do estado de um objeto e reconstituí-lo posteriormente. Esta seção apresenta uma noção da Java Serialization API e mostra como usá-la nos seus programas.

O que é serialização de objeto?

Serialização é um processo em que o estado de um objeto e seus metadados (como o nome da classe do objeto e os nomes de seus atributos) são armazenados em um formato binário especial. Colocar o objeto nesse formato —serializando-o— preserva todas as informações necessárias para reconstituir (ou desserializar) o objeto todas as vezes que for preciso fazer isso.

Há dois casos de uso principais para a serialização de objeto:

  • A persistência de objeto significa armazenar o estado do objeto em um mecanismo de persistência permanente, como em um banco de dados.
  • A composição remota do objeto significa enviar o objeto para outro computador ou sistema.

java.io.Serializable

A primeira etapa para fazer o trabalho de serialização é ativar os objetos para usar o mecanismo. Cada objeto que você deseja serializar deve implementar uma interface chamada java.io.Serializable:

import java.io.Serializable;
public class Person implements Serializable {
// etc...
}

A interface Serializable marca os objetos da classe Person para o tempo de execução como serializável. Cada subclasse de Person também será marcada como serializável.

Quaisquer atributos de um objeto que não sejam serializáveis farão que o tempo de execução do Java emita uma NotSerializableException se tentar serializar seu objeto. É possível gerenciar isso usando a palavra-chave transient para informar o tempo de execução a não tentar serializar determinados atributos. Nesse caso, você é responsável por assegurar que os atributos sejam restaurados de modo que o objeto funcione apropriadamente.

Serializando um Objeto

Agora, você tentará um exemplo que combina o que você acabou de aprender sobre o Java I/O com o que você aprendeu sobre serialização.

Suponha que você crie e preencha um objeto Manager (a recuperação desse Manager está no gráfico da herança de Person, que é serializável) e deseje serializar esse objeto para um OutputStream, neste caso, para um arquivo. Esse processo é mostrado na listagem 29:

Listagem 29. Serializando um objeto
Manager m = new Manager();
m.setEmployeeNumber("0001");

m.setGender(Gender.FEMALE);
m.setAge(29);
m.setHeight(170);
m.setName("Mary D. Boss");
m.setTaxpayerIdentificationNumber("123-45-6789");
log.info("About to write object using serialization... object looks like:");
m.printAudit(log);
try {
 String filename = "Manager-" + m.hashCode() + ".ser";
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
 oos.writeObject(m);
 log.info("Wrote object...");
} catch (Exception e) {
 log.log(Level.SEVERE, "Caught Exception processing object", e);
}

A primeira etapa é criar o objeto e definir alguns valores de atributos. Em seguida, você cria um OutputStream, neste caso, um FileOutputStream, e chama o writeObject() nesse fluxo. writeObject() é um método que usa a serialização do Java para serializar um objeto para o fluxo.

Neste exemplo, você está armazenando o objeto em um arquivo, mas a mesma técnica é usada par qualquer tipo de serialização.

Desserializando um Objeto

O ponto crucial da serialização é ser capaz de reconstituir ou desserializar o objeto. A listagem 30 lê o arquivo que você acabou de serializar, ou desserializar, seu conteúdo, restaurando o estado do objeto Manager:

Listagem 30. Desserializando um objeto
Manager m = new Manager();
m.setEmployeeNumber("0001");
m.setGender(Gender.FEMALE);
m.setAge(29);
m.setHeight(170);
m.setName("Mary D. Boss");
m.setTaxpayerIdentificationNumber("123-45-6789");
log.info("About to write object using serialization... object looks like:");
m.printAudit(log);
try {
 String filename = "Manager-" + m.hashCode() + ".ser";
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
 oos.writeObject(m);
 log.info("Wrote object...");

 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
 m = (Manager)ois.readObject();
 log.info("Read object using serialization... object looks like:");
 m.printAudit(log);
} catch (Exception e) {
 log.log(Level.SEVERE, "Caught Exception processing object", e);
}

Para a maioria dos propósitos do aplicativo, marcar seus objetos como serializable é tudo o que você sempre precisará se preocupar quanto o assunto é serialização. Nesses casos, quando for necessário serializar e desserializar seus objetos explicitamente, você poderá usar a técnica mostrada nas listagens 29 e 30 . Mas, à medida que o seu objeto de aplicativo evolua, e você inclua ou remova atributos dele, a serialização entra em uma nova camada de complexidade.

serialVersionUID

Lembra-se dos primeiros dias do middleware e da comunicação remota de objeto, os desenvolvedores eram muito responsáveis pelo controle do "formato de ligação" dos seus objetos, o que não diminuía a dor de cabeça à medida que a tecnologia evoluía.

Suponha que você inclua um atributo em um objeto, recompile e redistribua o código para cada máquina em um cluster de aplicativo. O objeto será armazenado em uma máquina com uma versão do código de serialização, mas acessado por outras máquinas que podem ter uma versão diferente de código. Quando essas máquinas tentam desserializar o objeto, coisas ruins frequentemente acontecem.

Os metadados da serialização do Java — as informações incluídas no formato de serialização binário — são sofisticados e resolvem muitos problemas que atormentavam os primeiros desenvolvedores de middleware. Mas não podem resolver todos os problemas.

A serialização do Java usa uma propriedade denominada serialVersionUID para ajudá-lo a lidar com diferentes versões de objetos em um cenário de serialização. Não é necessário declarar essa propriedade nos seus objetos; por padrão, a plataforma Java usa um algoritmo que calcula um valor para ele com base nos atributos da sua classe, o nome de classe e sua posição no cluster galáctico local. Na maior parte do tempo, isso funciona bem. Mas se você adicionar ou remover um atributo, esse valor gerado dinamicamente mudará, e o tempo de execução do Java emitirá uma InvalidClassException.

Para evitar que isso aconteça, desenvolva o hábito de declarar explicitamente um serialVersionUID:

import java.io.Serializable;
public class Person implements Serializable {
 private static final long serialVersionUID = 20100515;
// etc...
}

Recomendo usar algum tipo de esquema para o número de versão de serialVersionUID (no exemplo acima, eu usei a data atual), e declará-lo no private static final e no tipo long.

Você deve estar imaginando quando alterar essa propriedade. A resposta breve é que você deve alterá-la sempre que fizer uma mudança incompatível na classe, o que normalmente significa quando um atributo é removido. Se tiver uma versão do objeto em uma máquina que tenha o atributo removido, e o objeto for composto remotamente em uma máquina com uma versão do objeto na qual o atributo é esperado, então as coisas ficam estranhas.

Como regra geral, sempre que incluir ou remover os recursos (entenda-se atributos e métodos) de uma classe, altere seu serialVersionUID. É melhor obter uma InvalidClassException no outro lado da ligação do que obter um erro de aplicativo devido a uma mudança de classe incompatível.


Conclusão da Parte 2

O tutorial "Introdução à Programação Java" abordou uma parte significativa da linguagem Java, mas a linguagem é muito grande. Não é possível cobrir toda a linguagem em um único tutorial.

À medida que continuar aprendendo sobre a linguagem e a plataforma Java, provavelmente desejará estudar adicionalmente tópicos como expressões regulares, genéricos e serialização do Java. Eventualmente, talvez você queira explorar tópicos não mencionados neste tutorial de introdução, como concorrência e persistência. Outro tópico que deve ser explorado é o Java 7, que trará muitas mudanças potencialmente inovadoras para a plataforma Java. Veja Recursos para obter alguns bons pontos de partida para o aprendizado adicional sobre os conceitos da programação Java, incluindo aqueles que são muito avançados para serem abordados neste formato de introdução.

Recursos

Aprender

Obter produtos e tecnologias

  • JDK 6: Faça download do JDK 6 da Sun (Oracle).
  • Eclipse: Faça download do Eclipse IDE para Desenvolvedores de Java.
  • IBM Developer Kits: a IBM fornece diversos Developer Kits para uso em plataformas populares.

Discutir

  • Participe do comunidade My developerWorks. Entre em contato com outros usuários do developerWorks e explore os blogs, fóruns, grupos e wikis voltados para desenvolvedores.

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=658306
ArticleTitle=Introdução à Programação Java, Parte 2: Desenvolvimentos para Aplicativos Reais
publish-date=05172011