Laura Lemay e Charles L. Perkins
Os pacotes e as interfaces são duas capacidades que lhe permitem o maior controle e a flexibilidade no desenho de jogos de classes relacionadas. Os pacotes permitem-lhe combinar grupos de classes e controle que daquelas classes estão disponíveis para o mundo exterior; as interfaces fornecem um modo de agrupar definições de método abstratas e compartilhá-los entre classes que não necessariamente podem adquirir aqueles métodos por meio da herança.
Hoje aprenderá como projetar com, use e crie os seus próprios pacotes e interfaces. Os tópicos específicos sobre os quais aprenderá hoje incluem
Quando examina uma nova característica de língua, deve fazer-se duas perguntas:
O primeiro muitas vezes chama-se programando no grande, e o segundo, programando no pequeno. Bill Joy, fundador de Microsistemas de Sol, gosta de dizer que Java se sente como C programando no pequeno e como Smalltalk programando no grande. Com que quer dizer o que é que Java é familiar e potente como algum parecido a uma linguagem C enquanto codifica linhas individuais, mas tem a extensibilidade e o poder expressivo de uma língua orientada ao objeto pura como Smalltalk enquanto projeta.
A separação "do desenho" "da codificação" esteve um dos avanços mais fundamentais na programação durante as poucas décadas passadas e línguas orientadas ao objeto como instrumento de Java uma forma forte desta separação. A primeira parte desta separação já se descreveu em prévios dias: Quando desenvolve um programa Java, primeiro projeta as classes e decide as relações entre estas classes, e logo implementa o código de Java necessário para cada um dos métodos no seu desenho. Se for bastante cuidadoso com ambos estes processos, pode mudar de ideia sobre aspectos do desenho sem afetar algo exceto partes pequenas, locais do seu código de Java, e pode modificar a implementação de qualquer método sem afetar o resto do desenho.
Como começa a explorar a programação de Java mais promovida, contudo, encontrará que este modelo simples se torna também limitando. Hoje explorará estas limitações, para programar no grande e no pequeno, motivar a necessidade de pacotes e interfaces. Vamos começar com pacotes.
Os pacotes, como mencionado várias vezes neste livro por enquanto, são um modo de organizar grupos de classes. Um pacote contém qualquer número de classes que se relacionam no objetivo, no alcance, ou pela herança.
Porque preocupação com pacotes? Se os seus programas forem pequenos e usarem um número limitado de classes, pode encontrar que não precisa de explorar pacotes em absoluto. Mas mais Java programando-o faz, mais classes o encontrará têm. E embora aquelas classes possam projetar-se individualmente bem, reutilizáveis, encapsuladas, e com interfaces específicas para outras classes, pode encontrar a necessidade de uma entidade organizacional maior que lhe permite agrupar os seus pacotes.
Os pacotes são úteis por várias largas razões:
Embora um pacote seja o mais tipicamente uma coleção de classes, os pacotes também podem conter outros pacotes, formando ainda outro nível da organização um tanto análoga à hierarquia de herança. Cada "nível" normalmente representa um agrupamento mais pequeno, mais específico de classes. A própria biblioteca de classe de Java organiza-se ao longo destas linhas. O alto nível chama-se java; o seguinte nível inclui nomes como io, net, util e awt. O último destes tem um nível até mais baixo, que inclui o pacote image.
Observar |
Pela convenção, o primeiro nível da hierarquia especifica o (globalmente único) nome para identificar o autor ou o proprietário daqueles pacotes. Por exemplo, as classes de Microsistemas de Sol, que não são parte do ambiente de Java padrão, todos começam com o prefixo sun. As classes que o Netscape inclui com a sua implementação contêm-se no pacote de netscape. O pacote padrão, java, é uma exceção a esta regra porque é tão fundamental e porque poderia implementar-se um dia por múltiplas companhias. Lhe direi mais de convenções denominam o pacote depois quando criar os seus próprios pacotes. |
Tem usado pacotes do início neste livro. Cada vez quando usa a ordem de import, e cada vez refere-se a uma classe pelo seu nome de pacote cheio (java.awt.Color, por exemplo), usou pacotes. Vamos sobre a especificação de como usar classes de outros pacotes nos seus próprios programas para assegurar-nos que o tem e entrar na maior profundidade do que temos em lições prévias.
Usar uma classe conteve em um pacote, pode usar um de três mecanismos:
Que tal as suas próprias classes nos seus próprios programas que não pertencem a algum pacote? A regra consiste em que se não definir especificamente as suas classes para pertencer a um pacote, se põem em um pacote à revelia não denominado. Pode referir-se àquelas classes simplesmente pelo nome de classe de qualquer lugar no seu código.
Para referir-se a uma classe em algum outro pacote, pode usar o seu nome completo: o nome de classe precedido por qualquer nome de pacote. Não tem de importar a classe ou o pacote para usá-lo este caminho:
java.awt.Font f = new java.awt.Font()
Para classes que usa só algumas vezes no seu programa, usando o nome completo faz a maior parte de sentido. Se, contudo, usar aquela classe múltiplos tempos, ou se o nome de pacote for realmente longo com muitos subpacotes, quererá importar aquela classe em vez disso para salvar-se alguma datilografia.
Para importar classes de um pacote, use a ordem de import, como usou em todas as partes dos exemplos neste livro. Pode importar uma classe individual, como isto:
import java.util.Vector;
ou pode importar um pacote inteiro de classes, usando um asterisco (*) para substituir os nomes de classe individuais:
import java.awt.*
Observar |
De fato, para ser tecnicamente correta, esta ordem não importa todas as classes em um pacote - só importa as classes que se declararam por public, e até então só importa aquelas classes a que o próprio código se refere. Aprenderá mais nisto na seção intitulada "Pacotes e Proteção de Classe". |
Observe que o asterisco (*) neste exemplo não se parece com aquele que poderia usar em uma ordem pronta para especificar os conteúdos de um diretório ou indicar múltiplos arquivos. Por exemplo, se pede enumerar os conteúdos do diretório classes/java/awt/*, aquela lista inclui todos os arquivos de .class e subdiretórios, como image e peer. A escrita de import java.awt.* importa todas as classes públicas naquele pacote, mas não importa subpacotes como image e peer. Para importar todas as classes em uma hierarquia de pacote complexa, deve importar explicitamente cada nível da hierarquia à mão. Também, não pode indicar nomes de classe parciais (por exemplo, L* para importar todas as classes que começam com L). São todas as classes em um pacote ou uma classe única.
As afirmações de import na sua definição de classe vão em cima do arquivo, antes de qualquer definição de classe (mas depois da definição de pacote, como verá na seguinte seção).
Portanto deve não apressar-se para importar classes individualmente ou somente importá-los como um grupo? Depende de como específico quer ser. Importar um grupo de classes não diminui o seu programa ou fá-lo um pouco maior; só as classes que de fato usa no seu código se carregam como são necessários. Mas a importação de um pacote realmente fá-lo um pouco mais confuso para leitores do seu código para compreender onde as suas classes vêm de. Usar import individual s ou importar pacotes são pela maior parte uma pergunta do seu próprio estilo de codificação.
Nota técnica |
A ordem de import de Java não é de modo nenhum semelhante à ordem de #include no parecido às linguagem C, embora realizem funções semelhantes. O pré-processador C toma os conteúdos de todos os arquivos incluídos (e, à sua vez, os arquivos incluem, e assim por diante) e enche-os em no lugar onde o #include foi. O resultado é um pedaço enorme do código que tem muito mais linhas do que o programa original. import de Java comporta-se mais como um linker; diz ao compilador de Java e intérprete onde (em que arquivos) encontrar classes, variáveis, nomes de método e definições de método. Não traz nada no programa Java atual. |
Depois que importou uma classe ou um pacote de classes, pode referir-se normalmente a um nome de classe simplesmente pelo seu nome, sem o identificador de pacote. Digo "normalmente" porque há um caso onde deveria ser mais explícito: quando há múltiplas classes com o mesmo nome de pacotes diferentes.
Aqui está um exemplo. Digamos importa as classes de dois pacotes de dois programadores diferentes (Joe e Eleanor):
import joesclasses.*; import eleanorsclasses.*;
Dentro do pacote de Joe é uma classe chamada Name. Infelizmente, dentro do pacote de Eleanor também há uma classe chamada Name que tem uma significação inteiramente diferente e implementação. Iria se admirar cuja versão de Name terminaria de acostumar-se se mencionasse a classe de Name no seu próprio programa como isto:
Name myName = new Name("Susan");
A resposta não é nenhum; o compilador de Java vai se queixar de uma nomeação estão em conflito e recusam compilar o seu programa. Neste caso, apesar de que importou ambas as classes, ainda tem de referir-se à classe de Name apropriada pelo nome de pacote cheio:
joesclasses.Name myName = new joesclasses.Name("Susan");
Antes que continue explicando como criar os seus próprios pacotes de classes, eu gostaria de fazer uma nota sobre como Java encontra pacotes e classes quando compila e dirigindo as suas classes.
Para Java para ser capaz de usar uma classe, tem de ser capaz de encontrá-lo no sistema de arquivos. De outra maneira, adquirirá um erro que a classe não existe. Java usa duas coisas a encontrar classes: o próprio nome de pacote e os diretórios inclinaram-se na sua variável de CLASSPATH.
Em primeiro lugar, os nomes de pacote. O mapa de nomes de pacote de nomes do diretório no sistema de arquivos, portanto a classe java.applet.Applet vai se encontrar de fato no diretório applet, que à sua vez será dentro do diretório java (java/applet/Applet.class, em outras palavras).
Java procura aqueles diretórios, à sua vez, dentro dos diretórios enumerados na sua variável de CLASSPATH. Se se lembra atrás ao Dia 1, "Uma Introdução para a Programação de Java", quando instalou o JDK, teve de fundar uma variável de CLASSPATH para apontar para vários lugares onde as suas classes de Java vivem. CLASSPATH normalmente aponta para o diretório java/lib no seu lançamento de JDK, um diretório de classe no seu ambiente de desenvolvimento se tiver um, possivelmente algumas classes específicas para o browser, e ao diretório atual. Quando Java procura uma classe referiu na sua fonte, procura o pacote e nome de classe em cada um daqueles diretórios e devolve um erro se não puder encontrar o arquivo de classe. A maior parte "não pode carregar a classe" resultado incorreto por causa de variáveis de CLASSPATH faltadas.
Observar |
Se estiver usando a versão de Macintosh do JDK, está admirando-se provavelmente sobre que falo. Mac JDK não usa uma variável de CLASSPATH; sabe bastante para ser capaz de encontrar as classes à revelia e os contidos no diretório atual. Contudo, se faz muito desenvolvimento de Java, pode terminar com classes e pacotes em outros diretórios. O compilador de Java contém uma caixa de diálogo Preferences que o deixa acrescentar diretórios ao caminho de pesquisa de Java. |
A criação dos seus próprios pacotes é um processo difícil, complexo, implicando muitas linhas do código, horas longas tarde à noite com muito café e o sacrifício ritual de muitas cabras. Somente brincadeira. Para criar um pacote de classes, tem três passos básicos para seguir, que explicarei nas seções seguintes.
O primeiro passo deve decidir o que o nome do seu pacote vai ser. O nome que escolhe para o seu pacote depende de como vai estar usando aquelas classes. Possivelmente o seu pacote se chamará como você, ou possivelmente como a parte do sistema de Java continua a trabalhar (como graphics ou hardware_interfaces). Se estiver destinando o seu pacote a distribuir-se à Rede em liberdade, ou como parte de um produto comercial, quererá usar um nome de pacote (ou o jogo de nomes de pacote) que unicamente identifica você ou a sua organização ou ambos.
Uma convenção para denominar pacotes que se recomendou pelo Sol é usar o seu nome de domínio de Internet com os elementos invertidos. Deste modo, por exemplo, se o Sol seguisse a sua própria recomendação, os seus pacotes iriam se entregar à utilização do nome com.sun.java em vez de somente java. Se o seu nome de domínio de Internet é fooblitzky.eng.nonsense.edu, o seu nome de pacote poderia ser edu.nonsense.eng.fooblitzky (e poderia acrescentar outro nome de pacote para o fim disto para referir-se ao produto ou para você, especificamente).
A ideia é assegurar-se que o seu nome de pacote é único. Embora os pacotes possam esconder nomes de classe contrários, as paradas de proteção lá. Não há modo de assegurar-se que o seu pacote não estará em conflito com alguém o pacote de else se ambos vocês usarem o mesmo nome de pacote.
Pela convenção, os nomes de pacote tendem a começar com uma carta em letras minúscula a distingui-los de nomes de classe. Assim, por exemplo, no nome completo da classe de String construída, java.lang.String, é mais fácil separar o nome de pacote do nome de classe visualmente. Esta convenção ajuda a reduzir conflitos de nome.
O passo dois na criação de pacotes deve criar uma estrutura diretiva no seu disco que combina com o nome de pacote. Se o seu pacote tem somente um nome (mypackage), só terá de criar um diretório daquele um nome. Se o nome de pacote tem várias partes, contudo, terá de criar diretórios dentro de diretórios. Já que o pacote denomina edu.nonsense.eng.fooblitzky, precisará de criar um diretório edu e logo criar um diretório nonsense dentro de edu, um diretório eng dentro de nonsense e um diretório fooblitzky dentro de eng. As suas classes e os arquivos originais então podem ir dentro do diretório fooblitzky.
O passo final à colocação da sua classe dentro de pacotes deve acrescentar a ordem de package aos seus arquivos originais. A ordem de package diz que "esta classe vai dentro deste pacote" e se usa como isto:
package myclasses; package edu.nonsense.eng.fooblitzky; package java.awt;
A ordem de package única, se algum houver, deve ser a primeira linha do código no seu arquivo original, depois de qualquer comentário ou linhas em brancas e antes de qualquer ordem de import.
Como mencionado antes, se a sua classe não tiver uma ordem de package nela, aquela classe contém-se no pacote à revelia e pode usar-se por qualquer outra classe. Mas uma vez que começa a usar pacotes, deve assegurar-se que todas as suas classes pertencem a algum pacote para reduzir a possibilidade da confusão sobre onde as suas classes pertencem.
Ontem aprendeu todos sobre quatro Ps da proteção e como se aplicam (principalmente) a métodos e variáveis e a sua relação a outras classes. Referindo-se a classes e a sua relação a outras classes em outros pacotes, só tem dois Ps para incomodar-se com: pacote e público.
À revelia, as classes têm a proteção de pacote, que significa que a classe está disponível para todas as outras classes no mesmo pacote mas não é visível ou disponível do lado de fora daquele pacote - não até a subpacotes. Não pode importar-se ou mencionar-se de nome; as classes com a proteção de pacote escondem-se dentro do pacote no qual se contêm.
A proteção de pacote sucede quando define uma classe como tem em todas as partes deste livro, como isto:
class TheHiddenClass extends AnotherHiddenClass { ... }
Para permitir a uma classe ser visível e importable do lado de fora do seu pacote, quererá dar-lhe a proteção pública acrescentando o modificador de public à sua definição:
public class TheVisibleClass { ... }
As classes declaradas como public podem importar-se por outras classes do lado de fora do pacote.
Observe que quando usa uma afirmação de import com um asterisco, só importa as classes públicas dentro daquele pacote. As classes escondidas permanecem escondidas e podem usar-se só por outras classes naquele pacote.
Porque quereria esconder uma classe dentro de um pacote? Pela mesma razão quer esconder variáveis e métodos dentro de uma classe: portanto pode ter classes de serviço e comportamento que são só úteis para a sua implementação, ou portanto pode limitar a interface do seu programa para minimizar o efeito de modificações maiores. Como projeta as suas classes, quererá considerar o pacote inteiro e decidir que classes se declararão por public e que se esconderá.
A listagem 16.1 mostra duas classes que ilustram este ponto. O primeiro é uma classe pública que implementa uma lista ligada; o segundo é um nó privado daquela lista.
A listagem 16.1. A classe pública LinkedList.
1: package collections; 2: 3: public class LinkedList { 4: private Node root; 5: 6: public void add(Object o) { 7: root = new Node(o, root); 8: } 9: . . . 10: } 11: 12: class Node { // not public 13: private Object contents; 14: private Node next; 15: 16: Node(Object o, Node n) { 17: contents = o; 18: next = n; 19: } 20: . . . 21: }
Observar |
Note aqui que incluo duas definições de classe em um arquivo. Mencionei isto resumidamente no Dia 13, "Criando Interfaces de Usuário com o awt", e carrega mencionar aqui também: pode incluir tantas definições de classe por arquivo como quer, mas só um deles pode declarar-se por public, e aquele nome de arquivo deve ter o mesmo nome que uma classe pública. Quando Java compilar o arquivo, criará arquivos de .class separados de cada definição de classe dentro do arquivo. Na verdade, encontro que a correspondência individual da definição de classe entra muito mais facilmente mantido porque não tenho de ir procurar em volta a definição de uma classe. |
A classe de LinkedList pública fornece o grupo de métodos públicos úteis (como add()) a qualquer outra classe que poderia querer usá-los. Estas outras classes não precisam de saber sobre nenhuma classe de suporte LinkedList tem de fazer o seu emprego. Node, que é uma daquelas classes de suporte, por isso declara-se sem um modificador de public e não aparecerá como parte da interface pública para o pacote de collections.
Observar |
Somente porque Node não é público não significa que LinkedList não terá acesso a ele uma vez que se importou para alguma outra classe. Pense em proteções não como classes se escondem inteiramente, mas mais como verificação das permissões de uma classe dada de usar outras classes, variáveis e métodos. Quando importa e usa LinkedList, a classe de Node também se carregará no sistema, mas só os exemplos de LinkedList terão permissão de usá-lo. |
Um dos grandes poderes de classes escondidas é que mesmo se os usar para introduzir muita complexidade na implementação de alguma classe pública, toda a complexidade se esconde quando aquela classe se importa ou se usa. Assim, criar um bom pacote compõe-se de definir um jogo pequeno, limpo de classes públicas e métodos de outras classes para usar, e logo implementá-los usando qualquer número do escondido (pacote) classes de suporte. Verá outro uso de classes escondidas depois hoje.
As interfaces, como as classes abstratas e métodos que viu ontem, fornecem padrões do comportamento que se espera que outras classes implementem. As interfaces, contudo, fornecem muito mais funcionalidade a Java e classificar e objetar o desenho do que fazem classes abstratas simples e métodos. O resto desta lição explora interfaces: quais são, porque são cruciais para obter o máximo da língua de Java das suas próprias classes, e como usá-los e implementá-los.
Quando primeiro começa a projetar programas orientados ao objeto, o conceito da hierarquia de classe pode parecer quase miraculoso. Dentro daquela árvore única pode exprimir uma hierarquia de tipos diferentes de objetos, muitos simples a relações moderadamente complexas entre objetos e processos no mundo e qualquer número de pontos ao longo do eixo do abstrato/geral ao concreto/específico. A hierarquia estrita de classes parece, à primeira vista, ser simples, elegante, e fácil usar.
Depois de algum pensamento mais profundo ou experiência em planejamento mais complexa, contudo, pode descobrir que a simplicidade pura da hierarquia de classe é restritiva, em particular quando tem algum comportamento que tem de usar-se por classes em ramos diferentes da mesma árvore.
Vamos olhar para alguns exemplos que farão os problemas mais claros. O caminho atrás no Dia 2, "Programação orientada ao Objeto e Java", quando primeiro aprendeu sobre hierarquias de classe, discutimos a hierarquia de Vehicle, como mostrado na Figura 16.1.
A figura 16.1: A hierarquia Vechicle.
Agora vamos acrescentar àquela hierarquia e vamos criar as classes BritishCar e BritishMotorcycle embaixo de Car e Motorcycle, respectivamente. O comportamento que faz um britânico de motocicleta ou carro (que poderia incluir métodos de leakOil() ou electricalSystemFailure()) é comum em troca de ambas estas classes, mas porque estão em partes muito diferentes da hierarquia de classe, não pode criar uma superclasse comum para ambos. E não pode pôr o comportamento britânico além disso na hierarquia porque aquele comportamento não é comum em troca de todas as motocicletas e carros. Outro do que copiar fisicamente o comportamento entre as duas classes (que quebra as regras de programação orientada ao objeto [OOP] de reutilização de código e comportamento compartilhado), como pode criar uma hierarquia como isto?
Vamos olhar para um exemplo até mais espinhoso. Diga que tem uma hierarquia biológica com Animal em cima e as classes Mammal e Bird abaixo. As coisas que definem um mamífero incluem o carregamento pele viva jovem e que tem. O comportamento ou as características de pássaros incluem ter um bico e pôr ovos. Até aqui tudo bem, certo? Assim, como vai sobre a criação de uma classe do ornitorrinco, que tem a pele, tem um bico e põe ovos? Precisaria de combinar o comportamento de duas classes para formar a classe de Platypus. E, porque as classes podem ter só uma superclasse imediata em Java, este tipo do problema simplesmente não pode resolver-se elegantemente.
Outras línguas OOP incluem o conceito de múltipla herança, que resolve este problema. Com múltipla herança, uma classe pode herdar de mais de uma superclasse e adquirir comportamento e atributos de todas as suas superclasses ao mesmo tempo. Usando múltipla herança, poderia simplesmente o fator o comportamento comum de BritishCar e BritishMotorcycle em uma classe única (BritishThing) e logo criar novas classes que herdam tanto da sua superclasse primária como da classe de British.
O problema com múltipla herança consiste em que faz uma linguagem de programação muito mais complexa para aprender, usar e implementar. As perguntas da invocação de método e como a hierarquia de classe se organiza ficam muito mais complicadas com múltipla herança e mais abertas para confusão e ambiguidade. E porque uma das metas de Java foi que tenha sido simples, múltipla herança rejeitou-se a favor da herança única mais simples.
Assim, como resolve o problema de precisar do comportamento comum que não se ajusta na hierarquia de classe estrita? Java, que pede emprestado do Objetivo-C, tem outra hierarquia completamente separam-se da hierarquia de classe principal, uma hierarquia de classes de comportamento mixables. Então, quando cria uma nova classe, aquela classe tem só uma superclasse primária, mas pode escolher o dedo comportamentos comuns diferentes de outra hierarquia.
Esta outra hierarquia é a hierarquia de interface. Uma interface de Java é uma coleção do comportamento abstrato que pode misturar-se em qualquer classe para acrescentar àquele comportamento de classe que não se fornece pelas suas superclasses. Especificamente, uma interface de Java contém definições de método apenas abstratas e constantes - nenhuma variável de exemplo e nenhuma implementação de método.
As interfaces implementam-se e usam-se em todas as partes da biblioteca de classe de Java sempre que se espere que um comportamento se implemente por um número de classes desiguais. A hierarquia de classe de Java, por exemplo, define e usa as interfaces java.lang.Runnable, java.util.Enumeration, java.util.Observable, java.awt.image.ImageConsumer e java.awt.image.ImageProducer. Algumas destas interfaces viu antes; outros verá depois neste livro. Ainda os outros podem ser úteis para você nos seus próprios programas, assim com certeza examinarão o API para ver o que está disponível para você.
Em todas as partes deste livro adquiriu um gosto da diferença entre desenho e implementação na programação orientada ao objeto, onde o desenho de uma coisa é a sua representação abstrata e a sua implementação é a cópia concreta do desenho. Viu isto com métodos, onde a assinatura de um método define como se usa, mas a implementação de método pode ocorrer em qualquer lugar na hierarquia de classe. Viu isto com classes abstratas, onde o desenho da classe fornece um padrão para o comportamento, mas aquele comportamento não se implementa até além disso abaixo na hierarquia.
Esta distinção entre o desenho e a implementação de uma classe ou um método é uma parte crucial da teoria de programação orientada ao objeto. Pensar quanto a desenho quando organiza as suas classes permite-lhe adquirir o grande quadro sem atolar-se em detalhes de implementação. E ter o desenho total já definido quando de fato começa a implementar lhe permite concentrar-se naqueles detalhes sozinho da classe continua a trabalhar. Esta versão de programação de "pensa globalmente, atua localmente" fornece uma maneira de pensar potente sobre como as suas classes e os seus programas e os seus desenhos totais se organizam e como se relacionam.
Uma interface compõe-se do grupo de assinaturas de método sem implementações, fazendo-o a incorporação do desenho puro. Misturando uma interface em com a sua classe, abrange aquele desenho na sua implementação. Aquele desenho então pode incluir-se seguramente em qualquer lugar na hierarquia de classe porque não há detalhes específicos para a classe de como uma interface se comporta - nada para ignorar, nada para guardar a pista de, somente o nome e argumentos por um método.
Que tal classes abstratas? As classes abstratas não fornecem este mesmo comportamento? Sim e não. As classes abstratas e os métodos abstratos dentro deles realmente fornecem uma separação de desenho e implementação, permitindo-lhe ao fator o comportamento comum em uma superclasse abstrata. Mas as classes abstratas, e muitas vezes fazem, pode conter alguns dados concretos (como variáveis de exemplo), e pode ter uma superclasse abstrata tanto com métodos abstratos como com regulares, por meio disso confusos a distinção.
Mesmo uma classe abstrata pura com métodos só abstratos não é tão potente como uma interface. Uma classe abstrata é simplesmente outra classe; herda de alguma outra classe e tem o seu lugar na hierarquia. As classes abstratas não podem compartilhar-se através de partes diferentes da hierarquia de classe de maneira as interfaces podem, nem podem misturar-se em outras classes que precisam do seu comportamento. Para alcançar o tipo da flexibilidade do comportamento compartilhado através da hierarquia de classe, precisa de uma interface.
Pode pensar na diferença entre o desenho e a implementação de qualquer classe de Java como a diferença entre a hierarquia de interface e a hierarquia de desenho. A hierarquia de classe isoladamente herdada contém as implementações onde as relações entre classes e comportamento se definem rigidamente. Multiplicar hierarquia de interface mixable herdada, contudo, contém o desenho e pode usar-se livremente em qualquer lugar é necessário na implementação. Isto é uma maneira de pensar potente sobre a organização do seu programa, e embora tome um pouco que se acostuma a, também é altamente recomendado.
As classes e as interfaces, apesar das suas definições diferentes, têm um montão em comum. As interfaces, como classes, declaram-se em arquivos originais, uma interface para um arquivo. Como classes, também se compilam usando o compilador de Java em arquivos de .class. E, na maioria dos casos, em todo o caso pode usar uma classe (como um tipo de dados de uma variável, como o resultado de uma forma, e assim por diante), também pode usar uma interface.
Quase em todo lugar que este livro tenha um nome de classe em algum dos seus exemplos ou discussões, pode substituir um nome de interface. Os programadores de Java muitas vezes dizem "a classe" quando de fato significam "a classe ou a interface". O complemento de interfaces e estende o poder de classes, e os dois podem tratar-se quase exatamente o mesmo. Uma das poucas diferenças entre eles é que uma interface não pode ser instantiated: new só pode criar um exemplo de uma classe.
Agora que agarrou o que as interfaces são e porque são potentes (a "programação na grande" parte), vamos mudar a bits reais do código ("programando no pequeno"). Há duas coisas que pode fazer com interfaces: use-os nas suas próprias classes e defina o seu próprio. Vamos começar com o antigo.
Para usar uma interface, inclui a palavra-chave de implements como parte da sua definição de classe. Fez estas costas no Dia 11, "Mais Animação, Imagens e Som", quando aprendeu sobre fios e incluiu a interface de Runnable na sua definição applet:
// java.applet.Applet is the superclass public class Neko extends java.applet.Applet implements Runnable { // but it also has Runnable behavior ... }
Como as interfaces fornecem definições de método apenas abstratas, então tem de implementar aqueles métodos nas suas próprias classes, usando as mesmas assinaturas de método da interface. Observe que uma vez que inclui uma interface, tem de implementar todos os métodos naquela interface - não pode escolher o dedo os métodos dos quais precisa. Implementando uma interface diz aos usuários da sua classe que apoia toda daquela interface. (Observe que isto é outra diferença entre interfaces e as subclasses das classes abstratas do último podem escolher que métodos implementar ou ignorar e pode ignorar outros.)
Depois que a sua classe implementa uma interface, as subclasses da sua classe herdarão aqueles novos métodos (e pode ignorá-los ou sobrecarregá-los) exatamente como se a sua superclasse os tinha definido de fato. Se a sua classe herdar de uma superclasse que implementa uma interface dada, não tem de incluir a palavra-chave de implements na sua própria definição de classe.
Vamos examinar uma criação do exemplo simples a nova classe Orange. Suponha que já tem uma boa implementação da classe Fruit e uma interface, Fruitlike, que representa o que se espera que Fruit s seja capaz de fazer. Quer que uma laranja seja um fruto, mas também quer que ele seja um objeto esférico que pode lançar-se, fazer-se girar, e assim por diante. Aqui está como exprimir tudo isso (não se incomode com as definições destas interfaces por agora; aprenderá mais sobre eles depois hoje):
interface Fruitlike { void decay(); void squish(); . . . } class Fruit implements Fruitlike { private Color myColor; private int daysTilIRot; . . . } interface Spherelike { void toss(); void rotate(); . . . } class Orange extends Fruit implements Spherelike { . . . // toss()ing may squish() me (unique to me) }
Observe que a classe Orange não tem de dizer implements Fruitlike porque, estendendo Fruit, já tem! Uma das coisas bonitas sobre esta estrutura é que pode mudar de ideia sobre que classe Orange se estende (se uma classe de Sphere realmente grande se implementar repentinamente, por exemplo), ainda a classe Orange ainda entenderá as mesmas duas interfaces:
class Sphere implements Spherelike { // extends Object private float radius; . . . } class Orange extends Sphere implements Fruitlike { . . . // users of Orange never need know about the change! }
Diferentemente da hierarquia de classe isoladamente herdada, pode incluir tantas interfaces como precisa nas suas próprias classes, e a sua classe implementará o comportamento combinado de todas as interfaces incluídas. Para incluir múltiplas interfaces em uma classe, somente separe os seus nomes com vírgulas:
public class Neko extends java.applet.Applet implements Runnable, Eatable, Sortable, Observable { ... }
Observe que as complicações podem resultar da implementação de múltiplas interfaces - o que acontece se duas interfaces diferentes ambos definirem o mesmo método? Há três modos de resolver isto:
Lembre-se de que quase em todo lugar que pode usar uma classe, pode usar uma interface em vez disso. Deste modo, por exemplo, pode declarar que uma variável seja de um tipo de interface:
Runnable aRunnableObject = new MyAnimationClass()
Quando se declara que uma variável seja de um tipo de interface, simplesmente significa que se espera que qualquer objeto ao qual a variável se refere tenha implementado aquela interface - isto é, espera-se que entenda todos os métodos que a interface especifica. Supõe que uma promessa feita entre o desenhista da interface e os seus executores eventuais se tenha guardado. Neste caso, porque aRunnableObject contém um objeto do tipo Runnable, a suposição é que pode chamar aRunnableObject.run().
A coisa importante a realizar aqui está que embora se espere que aRunnableObject seja capaz de ter o método de run(), pode escrever este código muito antes de qualquer classe que se prepara implementam-se de fato (ou até criam-se!). Na programação orientada ao objeto tradicional, força-se a criar uma classe com implementações "de toco" (métodos vazios ou métodos que imprimem mensagens bobas) adquirir o mesmo efeito.
Também pode lançar objetos a uma interface, como pode lançar objetos a outras classes. Deste modo, por exemplo, vamos atrás àquela definição da classe de Orange, que implementou ambos a interface de Fruitlike (pela sua superclasse, Fruit) e a interface de Spherelike. Aqui lançaremos exemplos de Orange tanto a classes como a interfaces:
Orange anOrange = new Orange(); Fruit aFruit = (Fruit)anOrange; Fruitlike aFruitlike = (Fruitlike)anOrange; Spherelike aSpherelike = (Spherelike)anOrange; aFruit.decay(); // fruits decay aFruitlike.squish(); // and squish aFruitlike.toss(); // things that are fruitlike do not toss aSpherelike.toss(); // but things that are spherelike do anOrange.decay(); // oranges can do it all anOrange.squish(); anOrange.toss(); anOrange.rotate();
As declarações e as formas usam-se neste exemplo para restringir o comportamento de uma laranja à atuação mais como um mero fruto ou esfera.
Finalmente, observe que embora as interfaces se usem normalmente para misturar-se no comportamento a outras classes (assinaturas de método), as interfaces também podem usar-se para misturar-se em constantes geralmente úteis. Deste modo, por exemplo, se uma interface definiu o grupo de constantes, e logo múltiplas classes usaram aquelas constantes, os valores daquelas constantes podem modificar-se globalmente sem ter necessidade de modificar múltiplas classes. Isto ainda é outro exemplo de onde o uso de interfaces para separar o desenho da implementação pode fazer o seu código mais geral e mais facilmente que pode ser mantido.
Depois de usar interfaces durante algum tempo, o seguinte passo deve definir as suas próprias interfaces. As interfaces parecem muito a classes; declaram-se do modo quase o mesmo e podem arranjar-se em uma hierarquia, mas há regras para declarar interfaces que devem seguir-se.
Para criar uma nova interface, declara-o como isto:
public interface Growable { ... }
Isto é, efetivamente, o mesmo como uma definição de classe, com a palavra interface que substitui a palavra class. Dentro da definição de interface tem métodos e constantes. As definições de método dentro da interface são métodos de abstract e public; pode declará-los explicitamente como tal, ou vão se converter em public e métodos de abstract se não incluir aqueles modificadores. Não pode declarar que um método dentro de uma interface seja private ou protected. Deste modo, por exemplo, aqui está uma interface de Growable com um método explicitamente declarou public e abstract (growIt()) e um implicitamente declarado como tal (growItBigger()).
public interface Growable { public abstract void growIt(); //explicity public and abstract void growItBigger(); // effectively public and abstract }
Observe que, como com métodos abstratos em classes, os métodos dentro de interfaces não têm corpos. Lembre-se, uma interface é o desenho puro; não há implementação implicada.
Além de métodos, as interfaces também podem ter variáveis, mas aquelas variáveis devem declarar-se por public, static e final (fazendo-os constante). Como com métodos, pode definir explicitamente uma variável para ser public, static e final, ou vai se definir implicitamente como tal se não usar aqueles modificadores. Aqui está que a mesma definição de Growable com duas novas variáveis:
public interface Growable { public static final int increment = 10; long maxnum = 1000000; // becomes public static and final public abstract void growIt(); //explicitly public and abstract void growItBigger(); // effectively public and abstract }
As interfaces devem ter público ou proteção de pacote, como classes. Observe, contudo, que as interfaces sem o modificador de public não convertem automaticamente os seus métodos em public e abstract nem as suas constantes a public. Uma interface de non-public também tem métodos non-public e constantes que podem usar-se só por classes e outras interfaces no mesmo pacote.
As interfaces, como classes, podem pertencer a um pacote acrescentando uma afirmação de package à primeira linha do arquivo de classe. As interfaces também podem importar outras interfaces e classes de outros pacotes, como as classes podem.
Um truque para observar sobre métodos dentro de interfaces: supõe-se que aqueles métodos sejam abstratos e apliquem a alguma espécie da classe, mas como pode definir parâmetros àqueles métodos? Não sabe que classe os estará usando!
A resposta está no fato que usa um nome de interface em qualquer lugar um nome de classe pode usar-se, como aprendeu antes. Definindo os seus parâmetros de método para ser tipos de interface, pode criar parâmetros genéricos que aplicam a qualquer classe que poderia usar esta interface.
Deste modo, por exemplo, tome a interface Fruitlike, que define métodos (sem argumentos) para decay() e squish(). Também poderia haver um método de germinateSeeds(), que tem um argumento: o próprio fruto. De que tipo que o argumento vai ser? Não pode ser simplesmente Fruit, porque pode haver uma classe isto é Fruitlike (isto é, implementa a interface de Fruitlike) sem ser de fato um fruto. A solução é declarar o argumento como simplesmente Fruitlike na interface:
public interface Fruitlike { public abstract germinate(Fruitlike self) { ... } }
Então, em uma implementação real deste método em uma classe, pode tomar o argumento de Fruitlike genérico e lançá-lo ao objeto apropriado:
public class Orange extends Fruit { public germinate(Fruitlike self) { Orange theOrange = (Orange)self; ... } }
Como com classes, as interfaces podem organizar-se em uma hierarquia. Quando uma interface herda de outra interface, aquela "subinterface" adquire todas as definições de método e constantes que a sua "superinterface" definiu. Para estender uma interface, usa a palavra-chave de extends como faz em uma definição de classe:
public interface Fruitlike extends Foodlike { ... }
Observe que, diferentemente de classes, a hierarquia de interface não tem equivalente da classe de Object; esta hierarquia não se arraiga em nenhum ponto. As interfaces podem existir inteiramente sozinhos ou herdar de outra interface.
Também observe que, diferentemente da hierarquia de classe, a hierarquia de herança é multiplicam-se herdado. Deste modo, por exemplo, uma interface única pode estender tantas classes como precisa a (separado por vírgulas na parte de extends da definição), e a nova interface conterá uma combinação de métodos de todo o seu pai e constantes. Aqui está uma definição de interface de uma interface chamada BusyInterface que herda de um lote inteiro de outras interfaces:
public interface BusyInterface extends Runnable, Growable, Fruitlike, Observable { ...}
Em multiplicam interfaces herdadas, as regras de conflitos de nome de método gerentes são o mesmo quanto a classes que usam múltiplas interfaces; os métodos que só se diferenciam no tipo de regresso resultarão em um erro de compilador.
Para terminar a lição de hoje, aqui está um exemplo que usa pacotes, proteção de pacote, e define uma classe que implementa a interface de Lista (parte do pacote de java.util). A listagem 16.2 mostra o código.
A listagem 16.2. Pacotes, classes e interfaces.
1: package collections; 2: 3: public class LinkedList { 4: private Node root; 5: 6: . . . 7: public Enumeration enumerate() { 8: return new LinkedListEnumerator(root); 9: } 10: } 11: 12: class Node { 13: private Object contents; 14: private Node next; 15: 16: . . . 17: public Object contents() { 18: return contents; 19: } 20: 21: public Node next() { 22: return next; 23: } 24: } 25: 26: class LinkedListEnumerator implements Enumeration { 27: private Node currentNode; 28: 29: LinkedListEnumerator(Node root) { 30: currentNode = root; 31: } 32: 33: public boolean hasMoreElements() { 34: return currentNode != null; 35: } 36: 37: public Object nextElement() { 38: Object anObject = currentNode.contents(); 39: 40: currentNode = currentNode.next(); 41: return anObject; 42: } 43: }
Aqui está um uso típico do enumerador:
collections.LinkedList aLinkedList = createLinkedList(); java.util.Enumeration e = aLinkedList.enumerate(); while (e.hasMoreElements()) { Object anObject = e.nextElement(); // do something useful with anObject }
Note que, embora esteja usando o Enumeration e como se saiba qual é, de fato não faz. De fato, é um exemplo de uma classe escondida (LinkedListEnumerator) que não pode ver ou usar diretamente. Usando uma combinação de pacotes e interfaces, a classe de LinkedList conseguiu fornecer uma interface pública transparente para um pouco do seu comportamento mais importante (via a interface já definida java.util.Enumeration) encapsulando ainda (ocultação) da sua duas implementação (suporte) classes.
Distribuir um objeto como isto chama-se às vezes vendendo. Muitas vezes o "vendedor" distribui um objeto que um receptor não pode criar ele mesmo mas que sabe como usar. Por devolvê-lo ao vendedor, o receptor pode comprovar que tem certa capacidade, autentique-se ou faça qualquer número de tarefas úteis - todos sem saber muito sobre o objeto vendido. Isto é uma metáfora potente que pode aplicar-se em uma larga variedade de situações.
Hoje aprendeu como os pacotes podem usar-se para reunir e categorizar classes em grupos significativos. Os pacotes arranjam-se em uma hierarquia, que não só melhor organiza os seus programas mas permite você e milhões de programadores de Java fora na Rede denominar e compartilhar os seus projetos unicamente um com outro.
Também aprendeu como usar pacotes, tanto o seu próprio como muitos preexistentes na biblioteca de classe de Java.
Então descobriu como declarar e usar interfaces, um mecanismo potente para estender a herança única tradicional de classes de Java e para separar a herança de desenho da herança de implementação nos seus programas. As interfaces muitas vezes usam-se para chamar métodos comuns (compartilhados) quando a classe exata implicada não se conhece. Verá novos usos de interfaces amanhã e o dia depois.
Finalmente, aprendeu que os pacotes e as interfaces podem combinar-se para fornecer abstrações úteis, como LinkedList, que parecem simples ainda escondem de fato quase toda a sua (complexa) implementação dos seus usuários. Isto é uma técnica potente.
Pode usar import some.package.B* para importar todas as classes naquele pacote que começam com B? | |
Não, o asterisco de importação (*) não atua como um asterisco de linha de comando. | |
Então o que exatamente faz import ing com um * avaro? | |
A combinação de tudo disse anteriormente, esta definição exata emerge: importa todas as classes públicas que usa no seu código de Java que são diretamente dentro do pacote denominado, e não dentro de um dos seus subpacotes. (Só pode importar exatamente este jogo de classes, ou exatamente uma classe explicitamente denominada, de um pacote dado.) A propósito, Java só "carrega" a informação de uma classe quando de fato se refere àquela classe no seu código, portanto a forma de * de import não é menos eficiente do que a nomeação de cada classe individualmente. | |
Porque múltipla herança cheia é tão complexa que Java a abandonou? | |
Não é tanto que é demasiado complexo, mas que faz a língua demais complicada - e como aprenderá no Dia 21, "Abaixo do Capuz", isto pode fazer que a sistemas maiores sejam menos dignos de confiança e assim menos seguros. Por exemplo, se devesse herdar de dois pais diferentes, cada um que tem uma variável de exemplo com o mesmo nome, iria se forçar a permitir o conflito e explicar como os exatos a mesma referência para aquele nome da variável em cada uma das suas superclasses, e em você (todos os três), são diferentes agora. Em vez de ser capaz de chamar métodos "super" para realizar o comportamento mais abstrato, sempre precisaria de incomodar-se sobre o qual do (possivelmente muitos) métodos idênticos de fato desejou chamar em que pai. O despacho de método em tempo de execução de Java teria de ser mais complexo também. Finalmente, porque tantas pessoas estariam fornecendo classes para a reutilização na Rede, os conflitos normalmente manejáveis que surgiriam no seu próprio programa iriam se confundir por milhões de usuários que se misturam e e combinam com estes totalmente multiplicam classes herdadas à vontade. No futuro, se todas estas questões se resolverem, a herança mais potente pode acrescentar-se a Java, mas as suas capacidades atuais já são suficientes para 99 por cento dos seus programas. | |
As classes de abstract não têm de implementar todos os métodos em uma interface eles mesmos, mas fazer não todas as suas subclasses têm a? | |
De fato, não. Por causa da herança, a regra exata consiste em que uma implementação deve fornecer-se por alguma classe para cada método, mas não tem de ser a sua classe. Isto é análogo a quando é a subclasse de uma classe que implementa uma interface para você. Tudo o que a classe de abstract não implementa, a primeira classe non-abstract em baixo dela deve implementar. Então, mais longe as subclasses precisam de não fazer nada além disso. | |
Não mencionou rechamadas. Não são eles um uso importante de interfaces? | |
Sim, mas não os mencionei porque um bom exemplo seria demasiado grande. As rechamadas muitas vezes usam-se em interfaces de usuário (como sistemas de janela) para especificar o que o jogo de métodos vai enviar-se sempre que o usuário faça certo jogo de coisas (como clique no rato em algum lugar, datilografia, e assim por diante). Como as classes de interface de usuário não devem "saber" nada sobre as classes usando-os, a capacidade de uma interface de especificar que o grupo de métodos separados da árvore de classe é crucial para este caso. As rechamadas usando interfaces não são tão gerais como utilização, por exemplo, o método de perform: de Smalltalk, contudo, porque um objeto dado só pode solicitar que um objeto de interface de usuário "o chame atrás" utilização de um nome de método único. Suponha que o objeto quis dois objetos de interface de usuário da mesma classe de chamá-lo, usando nomes diferentes para distingui-los? Não pode fazer isto em Java, e força-se a usar estado especial e testes para distingui-los. (Avisei-o que se complicou!) Portanto embora as interfaces sejam bastante valiosas neste caso, não são a facilidade de rechamada ideal. |