[Pesquisar este blog]

quinta-feira, 15 de março de 2018

POO::Plena-20-Herança simples

POO-P-19-Especificadores e Modificadores POO-P-21-Sobreposição
Os objetos e entidades reais possuem relações entre si que, quando reconhecidas, facilitam o entendimento do conjunto de objetos envolvidos e, ao mesmo tempo, de suas partes. É bastante comum classificar as coisas, criando hierarquias de elementos, onde os grupos superiores contém características comuns, depois separadas em subgrupos, nos quais se incluem características mais específicas, possibilitando definir muitos níveis e grupos. A herança é o mecanismo da orientação a objetos que permite estabelecer este tipo de relação, possibilitando o compartilhamento de características comuns, criando famílias de classes.

A Biologia, por exemplo, organiza os seres vivos numa árvore, agrupando suas características comuns, separando-os em filos, ordens, gêneros e espécies a medida em que são diferenciados. Enquanto os mamíferos contém características comuns encontradas em todos os animais deste tipo, a ordem dos canídeos acrescenta características encontradas apenas nos cães e lobos; assim como a ordem dos felinos inclui características presentes em gatos e tigres. Nesta situação, podemos dizer que os canídeos e os felinos herdam características dos mamíferos. Da mesma forma, uma espécie particular de cão, herda as características dos canídeos, que por sua vez incluem as dos mamíferos. Assim, vemos que as características dos grupos superiores são propagadas, por meio da herança, para os grupos descendentes.

Herança

A herança (inheritance) é um dos mecanismos mais importantes da orientação a objeto s [1][2][4][5][6][7]. De fato, é uma técnica que possibilita a uma classe utilizar atributos e operações definidas em outra classe. Neste sentido a herança representa o compartilhamento de atributos e de operações entre classes, sendo uma característica que só existe na orientação a objetos.

Ao realizar tal compartilhamento, cria-se uma relação hierárquica, do tipo pai e filha, ou seja, a classe pai, tomada como ponto de partida, contém definições que, a critério do programador, poderão ser utilizadas nas classes definidas como filhas. Sob certos aspectos, é como se as características comuns de diversas classes fossem fatoradas em um tipo tomado como classe pai.

É comum que a classe pai seja chamada de classe base (base class) ou superclasse (superclass), enquanto a classe filha é denominada de classe derivada (derived class) ou subclasse (subclass). Nas linguagens de programação Java e C# é mais comum o uso dos termos superclasse e subclasse.

A figura que segue ilustra, por meio de um diagrama de classes UML, a relação existente entre uma superclasse e uma subclasse. Nele se observam as duas classes relacionadas, representadas por retângulos, unidas por uma reta finalizada por um triângulo não preenchido, que aponta para a classe de maior nível hierárquico, ou seja, a classe base ou superclasse. Uma superclasse pode dar origem a várias subclasses, sendo desnecessário dizer que tais subclasses pode ser empregada como novas superclasses, permitindo criar hierarquias com múltiplos níveis.

Além de compartilhar elementos existentes na superclasse, as subclasses podem adicionar novos atributos e operações próprios, ou seja, cada subclasse se torna particular e mais especial do que a superclasse. Assim as subclasses podem ser entendidas como extensões ou especializações de suas superclasses. Como também é possível substituir atributos e operações existentes, as características herdadas podem ser restritas ou modificadas, aumentando as possibilidades da herança. Quando características de uma classe são eliminadas, diz-se que ocorreu uma contração, pois subclasse torna-se mais simples que a superclasse.

No sentido mais amplo e também em sua forma de uso mais comum, a herança é um mecanismo para expressar similaridade entre classes, permitindo representar elementos comuns explicitamente em uma hierarquia de classes [2][4], possibilitando a adição de novos membros na criação de novos tipos derivados e especializados.

A existência de uma relação de herança entre dois tipos pode ser identificada quando uma das perguntas 'é um(a)?' ou 'é do tipo?' é respondida afirmativamente [4][6][7]. Ao considerar as classes Funcionario, Caixa e Cliente, é fácil perceber que Caixa é um (tipo de) Funcionario, mas não o contrário. Assim, Funcionario pode constituir a superclasse de uma hierarquia onde Caixa é uma de suas subclasses. Toda vez que a pergunta 'é um(a)?' determinar o relacionamento entre conceitos, deve ser considerado o emprego da herança na modelagem das classes envolvidas.

Ao mesmo tempo, Cliente não é um (tipo de) Caixa ou Funcionario, portanto constitui uma classe que não faz parte da hierarquia originada em Funcionario, apesar de constituir um dos tipos presentes no cenário considerado.

Quando uma aplicação é construída por meio de um conjunto de classes organizadas hierarquicamente, a ampliação de suas funcionalidades pode, muitas vezes, ser facilitada com a criação de novas classes baseadas nas classes originais. Assim, a herança constitui um mecanismo onde novos tipos de objetos podem ser rapidamente definidos em termos de outros existentes [12]. Outros tipos de funcionário, tais como Motorista, Segurança ou Gerente, poderiam ser facilmente adicionados atendendo os novos requisitos da aplicação. A figura que segue ilustra esta hierarquia possível.

Também deve ser destacado que este mecanismo permite que o conceito de reusabilidade seja verdadeiramente empregado, pois classes tomadas como base podem ser reutilizadas na definição de outras sem que seu código tenha que ser reproduzido ou modificado [6][7]. O emprego da herança evita a cópia direta de código, o que reduz substancialmente a propagação de erros quando os trechos de programação copiados se mostram inapropriados ou incorretos.

Existem duas formas de herança: a herança simples, onde uma única classe é tomada como base na criação de uma subclasse; e a herança múltipla, na qual duas ou mais classes são, simultaneamente, tomadas como base para definição de novas classes derivadas.

A herança simples é tratada neste post, enquanto a herança múltipla será discutida num próximo.

Herança simples

A herança simples é aquela onde a subclasse (classe filha ou classe derivada) é criada a partir de uma única superclasse (classe pai ou classe base), a qual compartilhará seus atributos públicos e protegidos. Esta forma de herança também pode ser conhecida como extensão simples.

A linguagem de programação Java oferece apenas a herança simples, cuja sintaxe requerida para expressar a criação deste tipo de relação é:

public NomeSubclasse extends NomeSuperclasse {
  // corpo da subclasse
}

A palavra reservada extends indica no nome da classe tomada como superclasse na declaração de uma nova subclasse.

Já C# oferece tanto a herança simples, como a múltipla. A sintaxe envolvida na criação de uma nova classe através do mecanismo da herança simples em C# é :

public class <NomeClasseDerivada> : <NomeSuperclasse> {
  // corpo da subclasse
}

Aqui o símbolo de pontuação ':' permite determinar o nome da classe tomada como superclasse na declaração de uma nova subclasse.

Tanto em Java como em C#, ao usar a sintaxe indicada acima, a subclasse passa a compartilhar todos os membros públicos e protegidos presentes na superclasse. Assim, os atributos e métodos presentes na superclasse podem ser livremente utilizados na programação da subclasse e, também das subclasses desta. Mas as instâncias da superclasse e da subclasse tem acesso apenas aos elementos públicos da superclasse.

Isto significa que um membro declarado público em uma classe sempre será acessível para todas as instâncias desta classe, para todas as subclasses e todas as instâncias de sua subclasses.

Os membros protegidos de uma classe poderão ser sempre utilizados na programação de subclasse e das subclasses das subclasses, mas nunca serão acessíveis para os objetos da classe, nem os objetos de quaisquer subclasses.

Finalmente, os elementos privados presentes na superclasse são completamente inacessíveis, o que evidencia a utilidade do níveis de acesso existentes.

Apesar das subclasses herdarem todos os membros públicos e protegidos de suas superclasses, os construtores, mesmo que públicos, não são herdados pelas subclasses, exigindo que novos construtores sejam supridos quando necessário. Sempre é importante destacar que, naturalmente, cada superclasse pode dar origem a um número qualquer de subclasses e, estas a outras subclasses.

A classe Java que segue, denominada Superclasse, declara três atributos a, b e positive; respectivamente dos tipos int, double e boolean; com acesso public, protected e private.

O atributo público a não possui métodos de acesso, pois não existem restrições para seu uso. Já o atributo protegido b, embora existam restrições para seus valores, quando tem conteúdo positivo, tal informação deve ser indicada pelo atributo privado positive como true, enquanto se b é negativo, positive deve ser false. Para garantir que estes dois atributos estejam corretamente relacionados, b é protegido e seu método de mutação setB(double) garante o valor adequado de positive para qualquer valor de b. O atributo positive, portanto, não pode sofrer ajustes diretos, possuindo apenas um método de acesso apropriado para seu tipo boolean, que é isPositive().

O único construtor existente garante que a inicialização do atributo b seja corretamente refletida pelo atributo positive. O método toString() facilita consultar o estado dos três atributos da classe.

public class Superclasse {
   public int a;
   protected double b;
   private boolean positive;

   public Superclasse() {
      setB(0.0);
   }

   public double getB() { return b; }
   public boolean isPositive() { return positive; }

   public void setB(double b) {
      if (b<0) positive = false;
      else positive = true;
      this.b = b;
   }

   public String toString() {
      return String.format("a = %d\nb = %f\npositive = %s\n",
         a, b, positive);
   }
}

O quadro que segue ilustra se os membros de Superclasse são acessíveis em seu código, por suas instâncias, no código de suas subclasses e por instâncias de suas subclasses. Os membros originais são aqueles declarados na própria classe.


O fragmento Java que segue mostra um possível uso da classe Superclasse.

Superclasse obj = new Superclasse();

System.out.println(obj);
obj.a = 13;
obj.setB(-1.5);
System.out.println(obj);

Sua execução produz a seguinte saída:

obj ==> a = 0
b = 0,000000
positive = true
obj ==> a = 13
b = -1,500000
positive = false

Uma classe Java derivada de Superclasse, denominada Subclasse, poderia ser como segue.

public class Subclasse extends Superclasse {
   public long l;
   protected double d;
   private double rate;

   public Subclasse() {
      super();
      // emprego explícito do construtor da superclasse
   }

   public double getD() { return d; }
   public double getRate() { return rate; }

   public void setD(double d) {
      if (d<0) {
         throw new RuntimeException("Valor inválido: " + d);
      }
      this.d = d;
      rate = b / d;
   }
}

Na subclasse denominada Subclasse, é possível ver que três novos atributos foram acrescentados. O atributo de tipo long l é público, pode ser usado irrestritamente e, por isso, não possui métodos de acesso ou mutação.

O atributo protegido de tipo long chamado d só aceita valores positivos, como garante o seu método de mutação associado setD(double). Além disso, valores válidos de d acarretam no armazenamento da razão b/d, onde b é um campo protegido da superclasse, tal como d é nesta classe. O valor da razão é obtido pelo método de acesso getRate().

O quadro que segue ilustra se os membros de Superclasse são acessíveis em seu código, por suas instâncias, no código de suas subclasses e por instâncias de suas subclasses. Os membros originais são aqueles declarados na própria classe, enquanto os membros herdados são aqueles oriundos de seus ancestrais (suas superclasses).


Também é importante notar que a classe Subclasse possui um construtor sem parâmetros, tal qual o default, que foi adicionado aqui apenas para mostrar o uso explícito de super.


super

A palavra reservada super indica a superclasse da classe onde onde é usado.

No caso de construtores, super só pode ser usado na primeira linha de código do construtor, explicitando o acionamento de um construtor (parametrizado ou não) da superclasse, que deve existir. Caso seja acionado o construtor default (sem parâmetros) da superclasse, a chamada a super() pode ser omitida.

Aqui é necessário destacar que quando a superclasse possui apenas construtores parametrizados, torna-se necessário

Em resumo, membros privados não são compartilhados, membros protegidos são acessíveis apenas no código de subclasses e membros públicos são acessíveis pelo código de subclasses e também por instâncias da classe e sua subclasses. Assim, fica a critério do projetista determinar quais os especificadores de acesso serão empregados em cada um dos membros das classes e dos membros adicionais das subclasses.

Usualmente os membros das classes devem ser privados, exceto nos casos onde não existam restrições para seu conteúdo. Métodos de acesso possibilitam usar membros privados indiretamente, garantindo a validade de seu conteúdo e a consistência com outros membros da classe, até mesmo quando não existem restrições. 

Os membros protegidos são destinados ao controle de aspectos internos da classe, mas que poderiam ser alvo de modificações em subclasses, desde que isto não comprometa a consistência de outros membros da classe. Quando o uso de um membro é muito complexo ou permite tornar o objeto inconsistente, é adequado declará-lo como privado, acrescentando métodos que permitam seu uso indireto e consistente.

A classe Object

Existe uma classe, tanto no Java como em C# que é muito importante. No Java, esta classe é Object, do pacote java.lang, que dá origem a toda hierarquia de classes do Java. No C# temos a mesma situação, a classe Object pertence ao namespace System. Todas as classes, de maneira direta ou indireta, possuem a classe Object como ancestral, ou seja, como superclasse. Isto ocorre porque toda vez que se declara uma nova classe, sem indicação explícita de uma classe base, é tomada implicitamente a classe Object como ancestral direto, o que cria uma raiz única para toda hierarquia de classes do Java ou do C#.

Observe que a declaração de um classe Horario, como abaixo, não indica, explicitamente uma superclasse:

public class Horario {
   private int hora;
   private int minuto;

   public Horario(int hora, int minuto) {
      setHora(hora);
      setMinuto(minuto);
   }

   public int getHora() { return hora; }
   public int getMinuto() { return minuto; }

   public void setHora(int hora) { 
      if (hora<0 || hora>23) {
         throw new RuntimeException("Hora invalida: " + hora);
      }
      this.hora = hora;
   }
   public void setMinuto(int minuto) {
      if (minuto<0 || minuto>59) {
         throw new RuntimeException("Minuto invalido: " + minuto);
      }
      this.minuto = minuto;
  }
}

Mas realmente esta declaração corresponde a:

// Java
public class Horario extends Object {
  :
}

// C#
public class Horario : Object {
  :
}

Assim, todos os objetos Java, incluindo qualquer tipo de array, compartilham os recursos disponíveis na classe Object, cuja API inclui elementos destinados à:
  • semântica de comparação (métodos equals(Object) e hashcode());
  • coleta de lixo (método finalize());
  • reflexão (método getClass());
  • representação textual (método toString()); e
  • sincronização de threads (métodos wait(), notify() e notifyAll()).
Em C# a situação é idêntica. Além disso, temos duas consequências importantes do fato de qualquer classe ser derivada de java.lang.Object ou de System.Object:
  • É a herança da classe Object que define uma infraestrutura comum a todos os objetos, possibilitando que acionem os métodos públicos lá definidos, como toString() e os demais.
  • Como toda classe é derivada de Object, todas as instâncias, quaisquer sejam suas classes específicas, podem ser tratadas como instâncias de Object, justificando porque muitas classes da API Java lidam com o tipo Object, pois isto permite operar com qualquer tipo de objeto.

Modificador final

O modificador final pode ser aplicado a uma declaração de atributo, método ou classe. Nestas três situações ele indica que a declaração onde se aplica é a definitiva, ou seja, que substituições ou modificações não serão aceitas.

Uma classe declarada como final não pode ser tomada como base de outra, ou seja, uma classe final não permite o uso da herança na construção de novas subclasses.

Assim, se a classe Horario fosse declarada como:

public final class Horario extends Object {
  :
}

Seria incorreto fazer:

public class HorarioDetalhado extends Horario {
  :
}

Se tal código for compilado, será produzido o erro:

HorarioDetalhado.java:1: error: cannot inherit from final Horario
public class HorarioDetalhado extends Horario {
                                      ^
1 error

Geralmente classes são feitas finais por motivos de segurança, quando não se deseja que terceiros modifiquem as classes existentes por meio de subclasses, garantindo a integridade do projeto.

Argumentos de um método que não devem ser modificados podem ser declarados como final na própria lista de parâmetros, mas como podem ser copiados e modificados indiretamente, o emprego desta declaração é meramente convencional, para comunicar tal intenção aos usuários do método.

O uso de final em atributos, quando associado ao modificador static, permite definir constantes, como mostrado no post Membros estáticos. Mas isoladamente impede que tal atributo seja modificado em subclasses. Da mesma forma, métodos declarados como final não poderão ser substituídos nas subclasses da classe onde foram declarados.

Estas duas últimas situações serão detalhadas no post sobre sobreposição.

Considerações finais

A herança simples é uma das características mais utilizadas da orientação a objeto na construção de sistemas, pois facilita o projeto ao permitir que classes contendo a infraestrutura de outras tenham seus membros compartilhados, seletivamente conforme determinado pelos seus especificadores de acesso, possibilitando a construção de outras, sem necessidade de copiar código, o que agiliza o desenvolvimento e simplifica as atividades de manutenção.


POO-P-19-Especificadores e Modificadores POO-P-21-Sobreposição

Referências Bibliográficas

[1] JAMSA, K.; KLANDER, L.. Programando em C/C++: a bíblia. São Paulo: Makron Books, 1999.
[2] PAGE_JONES, M.. Fundamentos do Desenho Orientado a Objeto com UML. São Paulo: Makron Books, 2001.
[3] SOMMERVILLE, I.. Software Engineering. 6th. Ed. Harlow: Pearson, 2001.
[4] DEITEL, H.M.; DEITEL, P.J.. Java: como programar. 6a. Ed. São Paulo: Pearson Prentice-Hall, 2005.
[5] SAVITCH, W.. C++ Absoluto. São Paulo: Pearson Addison-Wesley, 2004.
[6] JANDL JR., P. Introdução ao C++. São Paulo: Futura, 2003.
[7] JANDL JR., P.. Java - guia do programador. 3a. ed. São Paulo: Novatec, 2015.
[8] RUMBAUGH, J.; BLAHA, M.; PREMERLANI, W.; EDDY, F.; LORENSEN, W.. Object-oriented modeling and design. Englewoods Cliffs: Prentice-Hall, 1991.
[9] STROUSTRUP, B.. The C++ Programming Language. 3rd Ed. Reading: Addison-Wesley, 1997.
[10] LANGSAM, Y.; AUGENSTEIN, M. J.; TENENBAUM, A. M.. Data structures using C and C++. 2nd Ed. Upper Saddle River: Prentice-Hall, 1996.
[11] WATSON, K.; NAGEL, C.; PEDERSEN, J.H.; REID, J.D.; SKINNER, M.; WHITE, E.. Beginning Microsoft Visual C# 2008. Indianapolis: Wiley Publishing, 2008.
[12] GAMMA, E.; HELM, R.; JOHNSON, R.; VLISSIDES, J.. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.

domingo, 11 de março de 2018

POO::Plena-19-Membros Estáticos

POO-P-18-Especificadores e Modificadores POO-P-20-Herança Simples
Os membros estáticos são aqueles declarados com o modificador static, de maneira que tanto atributos/campos como métodos/operações possam ser definidas desta forma. Desta maneira é preciso uma conceituação mais precisa das implicações de seu uso para possibilita seu emprego adequado.

Como visto, as linguagens Java e C# possuem vários especificadores e modificadores (como no post anterior Especificadores e Modificadores), dentre os quais existe um denominado static. O uso deste modificador altera o comportamento dos membros assim declarados, tornando membros de instância em membros de classe, como veremos a seguir.

Membros de instância

Uma classe pode declarar vários membros:
  • Variáveis-membro (campos ou atributos) e
  • Funções-membro (métodos ou operações).
Estes membros, naturalmente, só podem ser utilizados pelas instâncias da classes, ou seja:
  • Cada objeto instanciado tem cópias exclusivas das variáveis-membro para seu uso individual; e
  • As funções-membro, acionadas pelos objetos instanciados, utilizam apenas as variáveis-membro de suas respectivas instâncias.
Por isso, tais membros são denominados de membros de instância.

Consideremos a classe Point dada a seguir.

public class Point {
    public double x;
    public double y;

    public void reflectX() { y = - y; }
    public void reflectY() { x = - x; }
}

Objetos do tipo Point podem ser instanciados como no fragmento abaixo, que usa o construtor default suprido pelo compilador:

Point p1 = new Point();
Point p2 = new Point();

Depois de criados, os objetos podem ser acessados por meio de suas referências, no caso p1 e p2, o que permite acessar as variáveis-membro x e y que são públicas.

p1.x =  5.5;
p1.y = 3.0;

p2.x = -1.5;
p2.y = 6.7;

Cada instância armazena seus próprios valores, retendo, portanto, apenas o seu próprio estado e, assim, o uso de métodos só afeta a instância onde são acionados.

System.out.println("P1: " + p1.x + ","+ p1.y);
System.out.println("P2: " + p2.x + ","+ p2.y);

p1.reflectY();
p2.reflectX();

System.out.println("P1: " + p1.x + ","+ p1.y);
System.out.println("P2: " + p2.x + ","+ p2.y);

Com isso fica claro que operações executadas numa instância não afetam quaisquer outras instâncias (do mesmo tipo ou de outros).

Membros de classe

Quando um membro de uma classe (variável ou método) é declarado como static, ele é modificado de maneira que deixa de pertencer às instâncias da classe, passando a pertencer a própria classe.

Assim uma variável-membro ou campo declarado como static torna-se uma variável de classe, um membro de classe ou um campo estático. Da mesma maneira, um método de instância ou operação torna-se um método de classe ou método estático.

O uso do modificador static não afeta os especificadores de acesso, de modo que static pode ser combinado com public, protected ou private, conforme desejado pelo programador.

Por outro lado, os construtores não podem, em qualquer circunstância, ser declarados como estáticos.

Analise a classe Cruise dada a seguir. Observe a existência de um campo estático denominado MAX e de um método estático chamado maxRate(double).

public class Cruise {
    public static double MAX;
    public double velocity;

    public boolean overMax () {
        return velocity > MAX;
    }

    public static double maxRate(double v) {
        return v/MAX;
    }
}

Objetos do tipo Cruise podem ser instanciados normalmente, com uso do construtor default.

Cruise c1 = new Cruise();
Cruise c2 = new Cruise();

O uso dos membros não estáticos não é afetado pela presença dos elementos estáticos, ou seja, é possível fazer:

c1.velocity =  81.5;
c2.velocity = 105.7;

if (c1.overMax()) System.out.println(c1.velocity);
if (c2.overMax()) System.out.println(c2.velocity);
// se executado este trecho exibe as velocidades 81.5 e 105.7.

O campo estático MAX é um membro de classe, portanto comum a todas as instâncias. Seu uso deve ser feito por meio do nome da classe, como segue:

Cruise.MAX = 100;

O novo valor de MAX é conhecido por todas as instâncias da classe, que tem acesso ao seu novo conteúdo. Assim, se executado

if (c1.overMax()) System.out.println(c1.velocity);
if (c2.overMax()) System.out.println(c2.velocity);
// agora este trecho exibe apenas a velocidade 105.7!

O método overMax(), não estático, emprega internamente a variável de instância velocity, a qual é comparada a variável de classe MAX. Na primeira execução, velocity do objeto c1 tem valor 81.5, velocity do objeto c2 tem valor 105.7, enquanto MAX, não inicializada, tem valor 0; produzindo os resultados 81.5 e 105.7, ambos valores maiores que MAX.

Alterando o valor da variável de classe MAX para 100 e executando novamente o método overMax() para as duas instâncias, temos que velocity do objeto c1 ainda tem valor 81.5, velocity do objeto c2 ainda tem valor 105.7, mas MAX, com novo valor 100; produz o resultado 105.7, única velocidade maior que MAX.

O uso de campos e métodos estáticos, conforme a recomendação da plataforma, deve usar o nome qualificado da classe, tal como:

Cruise.MAX = 100;
Cruise.maxRate(120);

Apesar disso, o uso de elementos estáticos por meio de instâncias, como segue, produz, quando muito, avisos por parte do compilador, pois o que define o acesso é, de fato, o especificador de acesso empregado.

c1.MAX = 120;
c2.maxRate(40);

O modificador static altera o comportamento do membro, que deixa de pertencer a instância, passando para o domínio da classe, acarretando em um conjunto de implicações que determinam a forma com que pode ser explorado.

Usos comuns de membros static

O uso do modificador static possibilita usos como:
  • Compartilhamento de informação entre instâncias de uma mesma classe, diretamente por meio de variáveis estáticas; ou indiretamente por meio de métodos estáticos que podem oferecer operações baseadas em valores comuns existentes entre as instâncias de uma classe.
  • Criação simplificada de biblioteca de funções, quando não é necessária a retenção de estado de objetos (feito por variáveis de instância)
  • Definição de constantes, que podem preservar de maneira imutável valores (e até mesmo referências de instâncias) considerados importantes em um programa.

Compartilhamento de informação

Os membros estáticos pertencem à classe, ou seja, eles são únicos e não dependem da existência de instâncias da classe (i.e., objetos) para serem acessados, apenas respeitando os especificadores de acesso combinados, sem quaisquer restrições adicionais.

Externamente, o acesso dos membros estáticos passa a ser qualificado pela própria classe, conforme sua visibilidade. Internamente o acesso dos membros estáticos é como qualquer outro.

Assim, se existe a classe Estatica como dado a seguir:
public class Estatica {
    public static int sval = 10;
    private int ival = 0;

    public static void reset() {
       sval = 0;
    }

    public int getIVal() { return ival; }
    public void setIVal(int i) { ival = i; }
}

É possível usar seus membros estáticos desta maneira:

// Uso de campo:
Estatica.sval = 33;
// Uso de método:
Estatica.reset();

Mas o uso dos membros não estáticos requer instâncias válidas:

Estatica e1 = new Estatica();
e1.setIVal(10);
System.out.println(e1.getIVal());

Assim todas as instâncias tem acesso aos membros estáticos únicos associados às suas próprias classes. Desta maneira, torna-se possível compartilhar valores entre instâncias por meio de variáveis estáticas, pois é como se fossem variáveis "globais" de um tipo específico.

A classe Estatica2 utiliza um membro estático denominado N que mantém o número de objetos da própria classe, incrementando-o na instanciação e decrementando-o na destruição de objetos.

public class Estatica2 {
    private static int N = 0;

    public Estatica2 () {
        N++;
    }
    protected void finalize() {
        N--;
    }
    public static int instances() {
        return N;
    }
}

Esta classe pode ser usada como segue:

// Exibe 0
System.out.println(Estatica2.instances());

Estatica2 obj1 = new Estatica2();
// Exibe 1
System.out.println(Estatica2.instances());

Estatica2 obj2 = new Estatica2();
// Exibe 2
System.out.println(Estatica2.instances());

obj1 = null; // GC pode atuar!
// Exibe 1 ou 2, conforme atuação do GC
System.out.println(Estatica2.instances());

Biblioteca de funções

Como os métodos estáticos não utilizam campos mutáveis de suas classes, podem oferecer operações independentes, cujo resultado se baseia, estritamente, nos argumentos recebidos.

Um conjunto de métodos estáticos relacionados, ou seja, cujas operações pertençam a um mesmo domínio, podem ser agrupados em uma única classe, a qual se comporta como uma biblioteca de funções

Um exemplo desta aplicação, na própria API do Java é a classe java.lang.Math, que só contém métodos estáticos que oferecem funções matemáticas independentes.

A classe Conversão, que segue, também contém apenas métodos estáticos relacionados a conversão de temperaturas entre diferentes unidades.

public class Conversao {
// conversão C --> F
   public static double C2F (double c) {
      return 9*c/5 + 32;
   }
// conversão F --> C
   public static double F2C (double f) {
      return 5*(f-32)/9;
   }
}

O uso desta classe não requer instâncias, pois os seus métodos podem ser acionados diretamente:

double x = 100;
double y= Conversao.C2F(x);
System.out.println("C = " + x + " => F = " + y);

y = 32;
x = Conversao.F2C(y);
System.out.println("F = " + y + " => C = " + x);

System.out.println("C = 24" + " => F = " + Conversao.C2F(24));


Definição de constantes

A linguagem Java não contém um modificador próprio para definição de constantes (como const em linguagem C++). Mas tal efeito pode ser obtido pela combinação de dois modificadores na declaração de variáveis:
  • static, que indica que membro pertence à classe;
  • final, que indica que membro não pode ser alterado, nem mesmo em subclasses.
Exemplos de declaração de constantes são:
public static final double PI = 3.1415926;
public final static int MAX = 123;
static public final byte MASK = 0b01001001;

Observe que a inicialização é obrigatória na declaração de constantes. Além disso, a ordem com que os modificadores e especificadores são aplicados não altera o efeito produzido.

Outro aspecto importante, é que a convenção de código Java solicita que os nomes das constantes seja grafado exclusivamente com letras maiúsculas, eventualmente com  uso de _ (underline) para separação de palavras.

Restrições dos membros static

Campos e métodos estáticos só podem utilizar:
  • Constantes literais;
  • Outros elementos estáticos;
  • Objetos e membros não estáticos de objetos diretamente instanciados em seu escopo.
De maneira geral, um membro estático não pode utilizar um elemento não estático se não for garantida sua existência (instanciação) no mesmo escopo.

Importação estática

Introduzida na versão 5 do Java, esta característica permite o uso de constantes declaradas em classes externas sem necessidade de sua qualificação (indicação do nome da classe).

Por exemplo, a classe Math contém constantes usualmente empregadas (de forma qualificada) como Math.PI ou Math.E, como no exemplo que segue.

// SEM importação estática
import java.lang.*;
public class SemImportStatic {
   public static void main(String args[]) {
      double raio = Double.parseDouble(args[0]);
      // uso da constante qualificada
      double perim = 2 * Math.PI * raio;
      System.out.print("raio = " + raio); 
      System.out.println(", perim = " + perim);
   }
}

Com uso da importação estática, torna-se possível não qualificar tais constantes, usadas diretamente como PI ou E, como na versão modificada do exemplo acima com uso desta alternativa.

// COM importação estática
import static java.lang.Math.*;
public class ComImportStatic {
   public static void main(String args[]) {
      double raio = Double.parseDouble(args[0]);
      // uso da constante qualificada
      double perim = 2 * PI * raio;
      System.out.print("raio = " + raio); 
      System.out.println(", perim = " + perim);
   }
}

A importação estática não tem impacto expressivo no código e, por isso, não é usada com muita frequência.

Concluindo, o uso de membros estáticos, como visto, é bastante conveniente para criação de um mecanismo simples de compartilhamento entre instâncias de uma classe, além de permitir a criação simplificada de bibliotecas de funções. Por estas razões, os membros estáticos são empregados frequentemente no código.


POO-P-18-Especificadores e Modificadores POO-P-20-Herança Simples

Referências Bibliográficas

[1] JAMSA, K.; KLANDER, L.. Programando em C/C++: a bíblia. São Paulo: Makron Books, 1999.
[2] PAGE_JONES, M.. Fundamentos do Desenho Orientado a Objeto com UML. São Paulo: Makron Books, 2001.
[3] SOMMERVILLE, I.. Software Engineering. 6th. Ed. Harlow: Pearson, 2001.
[4] DEITEL, H.M.; DEITEL, P.J.. Java: como programar. 6a. Ed. São Paulo: Pearson Prentice-Hall, 2005.
[5] SAVITCH, W.. C++ Absoluto. São Paulo: Pearson Addison-Wesley, 2004.
[6] JANDL JR., P. Introdução ao C++. São Paulo: Futura, 2003.
[7] JANDL JR., P.. Java - guia do programador. 3a. ed. São Paulo: Novatec, 2015.
[8] RUMBAUGH, J.; BLAHA, M.; PREMERLANI, W.; EDDY, F.; LORENSEN, W.. Object-oriented modeling and design. Englewoods Cliffs: Prentice-Hall, 1991.
[9] STROUSTRUP, B.. The C++ Programming Language. 3rd Ed. Reading: Addison-Wesley, 1997.
[10] LANGSAM, Y.; AUGENSTEIN, M. J.; TENENBAUM, A. M.. Data structures using C and C++. 2nd Ed. Upper Saddle River: Prentice-Hall, 1996.
[11] WATSON, K.; NAGEL, C.; PEDERSEN, J.H.; REID, J.D.; SKINNER, M.; WHITE, E.. Beginning Microsoft Visual C# 2008. Indianapolis: Wiley Publishing, 2008.