Herança Simples e Múltipla: Conceitos e Aplicações em Java e C++ Carlos Akcelrud Cony Programa Interdisciplinar de Pós-Graduação em Computação Aplicada Unisinos [email protected] Abstract Atualmente os conceitos que estão envolvidos na Orientação Objetos tem sido tema de diversos estudos e análises. Uma das principais caracterı́sticas da OO é o suporte ao conceito de Herança. A herança tem apresentado diversos benefı́cios para o desenvolvimento de softwares mais concisos, robustos e confiáveis mas isto tem seu custo. Já no que diz respeito as linguagens de programação que dão suporte a tal conceito, existem algumas divergências quanto o seu uso, estes temas serão abordados com mais ênfase neste artigo. 1 Introdução Neste artigo discutimos uma das tecnologias mais importantes que a orientação objetos suporta: Herança, que além de ser uma forma satisfatória de aperfeiçoar e organizar o código fonte, é uma técnica eficaz na redução da complexidade dos softwares. Além disso, será apresentado um comparativo entre Herança Simples e Herança Múltipla. Herança é uma técnica útil para ampliar a abstração de dados, apesar desta ser uma técnica efetiva para se definir um conceito único e claro, ela pode não ter o suporte devido para a solução completa do problema. Para [1], herança ”É o recebimento, por um componente de programa, de propriedades ou caracterı́sticas de outro componente de programa, de acordo com uma relação especificada entre os dois componentes”. 2 Herança Herança é utilizada para se representar a similaridade entre diferentes classes, com isso se simplifica a definição de Classes semelhantes a outras previamente definidas. Com isso é possı́vel que membros comuns, métodos e atributos sejam definidos somente uma vez (encapsulamento), e ainda especializar estes membros em determinados casos. Além do encapsulamento de dados, herança traz outros benefı́cios, tais como: permite à reutilização de software em que novas classes são criadas a partir de classes pré-existentes, absorvendo seus atributos e comportamentos adicionando novos recursos as classes antigas. A reutilização de software apresenta algumas vantagens, por exemplo, economiza e facilita no desenvolvimento de novos programas, incentiva a reutilização de códigos de alta qualidade já testados e depurados, reduzindo possı́veis problemas depois da instalação software final. A herança define uma relação entre classes do tipo ”é um”, onde uma classe compartilha a estrutura e o comportamento definidos em uma ou mais classes. A partir do reconhecimento da similaridade entre as classes definem uma hierarquia de classes, subdividida em: • Superclasses (classes bases): Representam abstrações mais gerais e se subdividem em duas: – Direta: A subclasse herda explicitamente da superclasse. – Indireta: É herdada de dois ou mais nı́veis acima na hierarquia de classes. • Subclasses (classes derivadas) Representam abstrações em que os atributos e os serviços especı́ficos são adicionados, modificados e/ou removidos. Para interligar as classes usa-se estruturas como: generalização e especialização, fazendo com que os atributos e serviços comuns fiquem claros em uma hierarquia de Classes. Então quando se dispõe de mecanismos de herança, a especificação de uma classe pode ser retirada de uma biblioteca existente e uma nova classe (Subclasse) pode ser criada a partir de uma classe pai (superclasse), através do processo de derivação. A classe filha pode especificar as suas caracterı́sticas que diferem da classe pai. Um problema apresentado pela herança é que uma subclasse pode herdar métodos (na maioria das vezes) que não necessita ou que não deveria os possuir, então o projetista deve assegurar que os recursos fornecidos por uma classe são apropriados para futuras subclasses. Contudo, então, a melhor idéia seria que um programador possuı́sse um conjunto de bibliotecas de classes (por exemplo, Java API) com código já implementado, à sua disposição, e então definiria quais destas seriam úteis para a confecção do novo software, tornando a implementação mais simples. Um exemplo de herança pode ser observado como na figura 1 onde se tem que um: • Estudante é uma Pessoa • Professor é uma Pessoa. • Um objeto da classe Estudante, tem os atributos e serviços de Estudante, e também os atributos herdados da classe Pessoa. Por exemplo, Carlos é um estudante e tem os atributos e serviços de Pessoa, como: Nome, Endereço, Dirigir, Viajar. Além destes possui atributos e serviços que expressam o comportamento especı́fico de estudante, como: Curso, Nota, Estudar e Realizar Prova. A classe Pessoa é reconhecida como uma generalização e Estudante como uma classe especialização, a qual herda os serviços e atributos de Pessoa. Além disso, uma classe derivada (uma especialização) pode incluir novos serviços e atributos ou redefinir os serviços herdados. 2.1 Este é o tipo mais trivial de construção de subclasses, e tem como objetivo central a especialização da nova classe tornando-a mais especı́fica que a classe pai, satisfazendo em todos os aspectos relevantes as especificações do pai. Este tipo de herança é a mais comum e mais utilizada. A seguir um exemplo deste tipo de herança: Uma classe ”janela”, dispõe de operações gerais de janela como mover, redimensionar,... Uma subclasse especializada ”janela de texto” herda as operações de ”janela” e em adição fornece ainda outras facilidades que permitem, por exemplo, fazer o display de texto, editar texto,..., mas mantêm as propriedades de uma janela em geral. [7] 2.2 Dessa forma, Funcionário pode incluir atributos como Salário e Profissão e serviços como CalcularSalário, que podem não ser relevantes ou apropriados para Pessoa em geral. Subclasse por Especificação Este método também é utilizado de maneira freqüente se faz quando as classes pai e derivadas mantêm um interface comum, isto é apresentam os mesmos métodos, ou seja, a classe pai é uma combinação de operações implementadas e operações que só serão implementadas nas classes filhos. Muitas vezes não há alteração da interface entre a classe pai e a classe filho, o filho que implementará o comportamento descrito, já que este não implementado no pai. Nestes casos as subclasses não são refinamentos dos tipos que existem, mas antes realizações de uma especificação abstrata incompleta. Subclasses por especificação podem então ser reconhecidas quando, a superclasse não implementa o comportamento atual, mas descreve-o simplesmente e é implementado nas subclasses. 2.3 Figure 1. Classe Pessoa e Seus Herdeiros Subclasse por Especialização Subclasse por Generalização A construção de subclasses por generalização é de certa forma o contrário de herança por subclasses por especialização. Neste tipo, uma subclasse estende o comportamento da classe pai para criar um objeto mais geral, é muitas vezes aplicável quando se quer construir a partir de uma base existente que não se quer ou não se pode alterar, por exemplo, um sistema de display de gráficos em que a classe ”janela” foi definida para fazer o display com fundo branco e preto. Quando se cria um subtipo, ”classe colorida”, que permita que o fundo seja de outra cor que não branco e preto, juntase um campo adicional para guardar a cor e sobre-se ao código da ”‘janela”’ herdada em que o fundo é desenhado nessa cor. Subclasses por generalização ocorrem mais freqüencia quando o projeto é baseado em dados e coloca para 2o lugar questões de comportamento. 2.4 Subclasses por extensão Enquanto as subclasses por generalização modificam ou expandem as funcionalidades de um objeto, subclasses por extensão juntam funcionalidades completamente novas. A diferença entre esta forma de herança da anterior é o fato que esta sobrepõe algum método da classe pai enquanto em subclasses por extensão, são agregados novos métodos aos do pai, já as funcionalidades são menos ligadas aos métodos do pai, por exemplo, pretende-se manter os objetos de certa classe numa lista, para isto, pode-se, então, construir uma subclasse em que se unem os campos necessários para a construção de uma lista encadeada. [7] Com isso a funcionalidade do pai permanece intocável. 2.5 Subclasses por Limitação Quando a classe filha é menor, ou contém mais restrições que a classe pai, então herança de subclasse por limitação ocorre. Semelhantemente a subclasse por generalização, a subclasse por limitação aparece no momento da programação da nova classe a partir de classes pré-existentes e percebe-se que não se quer alterar ou não se pode, por exemplo, uma classe que traduzia uma fila de espera dupla em que os elementos podiam ser juntos ou retirados de cada um dos lados da estrutura. O programador quer representar uma classe pilha, reforçando a propriedade de que os elementos só podem ser adicionados ou retirados de um dos lados da pilha. [7] Como resultado, surge uma subclasse que modificou ou sobrepôs alguns métodos e eliminou outros que não interessavam mais. 2.6 Subclasses por Combinação Método semelhante ao utilizado pela herança múltipla, ou seja, uma combinação entre duas ou mais classes, um professor Assistente tem caracterı́sticas representativas das duas seguintes classes: Professor e Aluno. [7] 3 3.1 O nome da classe pai pode ser precedido pelas palavras chave private ou public. Se as palavras chaves não existirem, a classe derivada será por padrão, privadamente derivada. (figura 2) Se a classe filho for declarar de maneira publica, então o que é público aos que usam a classe base, será público para os utilizadores da classe filha, a não ser que um método seja sobrecarregado na filha. Já se o filho é uma classe privada, então nada do pai pode ser público para os usam a classe filha. Herança Simples Herança Simples em C++ A declaração em C++ de uma classe derivada pode fazer-se do seguinte modo: class¡nome da classe derivada¿:¡nome da classe pai¿ class¡nome da classe derivada¿:private¡nome da classe pai¿ class¡nome da classe derivada¿:public¡nome da classe pai¿ Figure 2. A classe derivada não pode ascender aos dados privados da classe pai, para que a proteção da classe não seja quebrada só pelo processo de derivação. Pode, no entanto, ser especificamente autorizada para isso. Assim, existem dois processos: • Construção de funções friend (ou a classe inteira ou alguns métodos). • Membros declarados na parte protegida que são acedidos pelas classes derivadas. A partir disto é criado uma maneira diferente para o acesso, protected. Um método ou atributo protegido comportase como um membro público para a classe filha, mas é considerado privado para o resto do programa. (Figura 3) Desta maneira as funções membro do filho podem ascender, por exemplo, a uma variável declarada na zona protegida da classe pai, mas os utilizadores da classe pai ou da classe filho não podem o fazer. Uma melhor compreensão considere o exemplo a seguir: uma classe base designada pelo pai, deriva-se duas outras subclasses, filho1, privadamente e filho2, publicamente. Então, sempre que uma classe derivada redefine uma função membro pública da classe base, se pretende fazer atuar sobre um objeto da classe derivada, o método da classe pai 3.2 Herança Simples em Java Herança Simples em Java se vale dos mesmos conceitos que na linguagem C++, então será mostrado um exemplo para implementar um applet, que utiliza-se a seguinte notação: public class HelloApplet extends JApplet { ...... } A palavra chave extends indica que HelloApplet é uma classe filha de JApplet. Isso significa que a classe HelloApplet herda por exemplo, os métodos init() e start() de JApplet. Em um applet, pode-se definir novos atributos e métodos, bem como sobrescrever os métodos herdados de JApplet, como o init(). Um exemplo de código fonte de herança simples em Java: Figure 3. que foi redefinido, tem-se que usar o operador de âmbito de resolução. public class Student extends Person { private String grade; private int classes; ... public Student(String n, String g) { No exemplo, abaixo, mostra-se como é feito o código de herança simples em C++, onde a classe ”planos” herda os métodos públicos de Vetores. class Vetores { private: struct vetor v,p; public: super(n); grade = g; classes = 0; } public String toString() { return super.toString() + ”: ” + grade; } } void cria(float,float,float,float,float,float); void cria(vetor,vetor); void vetores (float, float, float); float escalar (vetor,vetor); vetor vetorial (vetor,vetor); float projecao(vetor,vetor); float modulo(vetor); vetor produto (float,vetor); vetor subtracao (vetor,vetor); }; A classe acima ilustra a criação de uma subclasse Student que herda os atributos e métodos de Person. Uma subclasse não herda os construtores de sua superclasse, mas pode invocá-los através do comando super [8]. A chamada a essa função deve ser a primeira instrução no construtor da subclasse, senão ocorrerá erro. Caso a chamada ao construtor base não tivesse sido feita explicitamente em Student(String n, String g), uma chamada ao construtor default (sem argumentos) da classe base seria realizada. class Plano: public Vetores { private: 4 vetor normal; void set pontos(float, float,float); void set normal(vetor, vetor); public: void cria (vetor, vetor, vetor); void cria (float,float,float,float,float); vetor get normal(); }; Herança Múltipla É a capacidade de uma classe herdar de duas ou mais classes, por exemplo à classe rádio-relógio herdar da classe rádio e da classe relógio. [9] Vantagens: • Reuso de classes • Simula o pensamento humano, por exemplo, um estagiário é um estudante e um funcionário ao mesmo tempo Desvantagens: • Perda da simplicidade conceitual • Dificuldade em implementar • Definição da regra de ativação de métodos herdados • Ambigüidade ocorre quando as superclasses possuem homônimos e a subclasse não redefine esses membros em suas declarações, causando problemas para o compilador. (Figura 4) Concluı́-se que, como regra geral, deve-se evitar o seu uso, exceto para inclusão de diferentes interfaces independentes. 4.1 Herança Múltipla em C++ Herança Múltipla permite a inserção de conceitos diferentes, tais como, uma classe pode ser combinada com mais de uma superclasse para formar uma nova classebase, por exemplo a Figura 5. [2] construtor da classe derivada aparecerá: [9] D :: D (int x,int y, int z, int w, int t) : B1 (z) , B2 (w,t) Caso uma classe filha declare um método igual a um pré-existente na classe pai, então a função da classe filha esconde a função da classe pai, mesmo que tenham argumentos de tipos diferentes (overriding). Um exemplo retirado de [6] de código fonte de herança múltipla em C++: class B1 {int atr b1;}; class B2 {int atr b2;}; class D: public B1, public B2 4.1.1 Herança Múltipla Virtual em C++ O problema da duplicidade é quando existem dois slots de memória que estão referenciando a mesma variável, ou dois caminhos diferentes para a chamada de uma função, e com herança múltipla esse tipo de problema pode surgir [5], para resolver isto o C++ dá suporte a um recurso que se chama superclasse virtual. (Figura 6) Figure 4. Ambigüidade em Herança O C++ suporta herança múltipla, permitindo que uma classe seja derivada de várias classes base. [3] Para tal, a definição da classe derivada, terá a seguinte configuração: class ¡nome da classe ¿ : [public] ¡nome da classe¿,..., [public] ¡nome da classe ¿ Ao utilizar construtores, quando se faz a declaração do construtor da classe derivada, listam-se os construtores das classes base com os valores dos argumentos, por exemplo: A classe derivada D terá como classes base a classe B1, cujo construtor tem um argumento e B2 cujo construtor tem, por exemplo, dois argumentos. O construtor da classe D tem dois argumentos. Assim quando se define o Figure 5. Conceito de Herança Múltipla A declaração virtual soluciona o problema da ambigüidade da duplicidade de atributos, apenas eliminando a criação de dois endereços de memória para atributos na superclasse. Um exemplo, retirado de [6], de código fonte de Herança Múltipla Virtual: class B0 {int atrb 0; }; class B1: virtual public B0 {int atrb 1; }; class B2: virtual public B0 {int atrb 2; }; class D: virtual public B1, virtual public B2 {int atr dl}; Figure 6. Herança Múltipla Virtual 4.2 Herança Múltipla em JAVA Da mesma maneira que C++, Java suporta herança de objetos, mas não suporta herança múltipla. Em seu lugar Java admite uma nova construção chamada ”interface”. As interfaces especificam o comportamento de um objeto sem definir a sua implementação. [4] Java suporta a herança múltipla de interfaces com isso ganha-se muitos dos benefı́cios da herança múltipla de classes sem seus inconvenientes. Interface é uma coleção de definições de métodos (sem implementação) e constantes. Na declaração de uma classe pode constar que ela implementa uma ou mais interfaces. Algumas caracterı́sticas das interfaces são: • Não é possı́vel herdar variáveis; • Não é possı́vel herdar implementação de métodos; • A hierarquia de Interfaces é independente da hierarquia de Classes. Duas classes que implementam a mesma Interface não são necessariamente relacionadas na hierarquia de Classes. Vejamos uma utilidade das interfaces. Suponhamos que queremos fazer referência a um certo conjunto de classes que possuem o método func(), mesmo que não tenham uma superclasse comum a não ser a classe base Object. Para isso, primeiro declaramos uma interface que especifica o método func(). Em seguida criamos subclasses dessas classes, que implementam essa interface. Agora, para fazer referência às classes, basta fazer referência à interface comum Uma interface é utilizada quando não existe a necessidade das classes derivadas herdarem métodos já implementados, e são um conjunto de métodos e constantes (não contém atributos). Então, Java por motivos de simplicidade, abandona a idéia de herança múltipla, cedendo lugar ao uso de interfaces. Os métodos definidos na interface são ”ocos” ou desprovidos de implementação. Classes podem dizer que implementam uma interface, estabelecendo um compromisso, uma espécie de contrato, com seus clientes no que se refere a prover uma implementação para cada método da referida interface. Ao cliente, pode ser dada a definição da interface, porém, ele acaba não sabendo o que a classe é, mas sabe o que faz. Um exemplo de herança ”múltipla” em Java: public class Analyse extends Runnable{ private Words[] arrayWord; private POS[] arrayPOS; private Chunks[] arrayChunks; ... } 5 Thread implements Java versus C++ Neste tópico serão mostradas algumas das principais diferenças entres as linguagens de programação Java e C++, para isso, observe a tabela 1: 6 Conclusões • Reusabilidade de código. Quando comportamento é herdado de uma outra classe, o código que o fornece não precisa ser reescrito. Isto é importante, já que os programadores perdem seu do tempo reescrevendo código. Por exemplo, para encontrar um padrão em uma lista ou inserir um novo elemento em uma tabela. Com técnicas de OO, estas funções podem ser escritas apenas uma vez e reutilizadas. • Compartilhamento de código. Ocorre em muitos nı́veis com técnicas OO. Uma forma se dá quando diversos programadores ou softwares podem usar as mesmas classes. Uma outra ocorre quando duas ou mais classes desenvolvidas por um programador como parte de um projeto herda de uma única classe pai. Quando isto acontece, dois ou mais tipos de objetos compartilharão o código que eles herdaram. • Consistência de interface. Quando duas ou mais classes herdam da mesma classe pai, podemos estar certos de que o comportamento que elas herdaram será o mesmo em todas as classes. Logo, é garantido que as interfaces para objetos semelhantes sejam, de fato, semelhantes. C ++ Definição da classe e implementação em ficheiros separados. Usa funções e métodos. Caso especial: função main(). Construção de objetos em modo dinâmico e não dinâmico. Especificador de acessibilidade: Os atributos com omissão de especificador são privados. Se uma classe não declara explicitamente o construtor por defeito então o compilador criará automaticamente um, exceto se já existir mais algum construtor. Necessário implementar construtor cópia, destrutor e fazer a sobrecarga do operador de atribuição quando a classe é composta por atributos do tipo apontador. Arrays de objetos: Na criação de um array de objetos, é invocado automaticamente o construtor sem parâmetros para inicializar cada um dos objetos do array. Passagem de parâmetros: por valor, por apontador e por variável referência. Java Definição da classe e implementação no mesmo ficheiro. (Estrutura de classe.) Só métodos. O método main() terá de estar incluı́do numa classe. Construção de objetos sempre em modo dinâmico. Os atributos com omissão de especificador são acessı́veis às classes dentro do mesmo package. Idêntico ao C++ Não é preciso implementar qualquer um dos métodos referidos. No entanto, o uso do método clone() em Java deve ser usado sempre que se pretende cópia de um objeto. Um array de objetos é criado com a instrução new e cada um dos seus objetos necessita também de ser criado de modo explı́cito (new). Por cópia da referência do objeto (equivalente à passagem por valor de um apontador em C++/C). Tabela 1 • Componentes de software. Herança permite que programadores construam componentes de software reutilizáveis com o objetivo de desenvolver aplicações novas que requeiram pouca ou nenhuma codificação. • Prototipação rápida. Quando um sistema de software é construı́do com base em componentes reutilizáveis, o tempo de desenvolvimento pode ser concentrado em entender a parte nova do sistema. Logo, sistemas de software podem ser gerados mais facilmente e mais rapidamente. • Ocultamento de informação. Um programador que reutiliza um componente de software necessita apenas entender a natureza do componente e sua interface. Logo, a interconexão entre sistemas de software é reduzida e a complexidade também é reduzida. Embora os benefı́cios de herança em programação OO sejam grandes, este mecanismo, não possui custo zero. Por isso, devemos considerar o custo de técnicas OO e, em particular, o custo de herança: • Velocidade de execução. Dificilmente, ferramentas de software de propósito geral são tão rápidas quanto aquelas desenvolvidas cuidadosamente para um propósito especı́fico. Logo, métodos herdados, que devem lidar com subclasses arbitrarias são frequentemente mais lentos do que código especializado. • Tamanho do programa. O uso de qualquer biblioteca de software em geral acarreta o aumento de tamanho do programa, o que não acontece com sistemas construı́dos através de um projeto especı́fico. • Overhead de envio de mensagens. Muito do que tem sido feito do fato que envio de mensagens é por natureza uma operação mais custosa do que chamada de procedimento. • Complexidade do programa. Embora a programação OO seja tida como uma solução para a complexidade do software, o uso demasiado de herança pode simplesmente substituir uma forma de complexidade por outra. Entender o fluxo de controle de um programa que utiliza herança pode requerer várias varreduras no grafo de herança. Em relação a herança nas linguagens de programação Java e C++, a discussão deve ser feita de maneira cautelosa. Enquanto o Java tem por caracterı́sticas a simplicidade, robustez, segurança e portabilidade, o C++ apresenta conceitos mais complexos, a mais tempo no mercado e, ainda, na maioria dos casos é mais rápida por não possuir uma máquina virtual. Portanto, o uso da linguagem de programação fica a cargo do programador, e na análise de seu problema. References [1] PRATT, T. W.; ZELKOWITZ, M.V., 2001 [2] DEITEL, H.M.; DEITEL, P.J. C++: Como Programar. Bookman. [3] GRAHAM, Neill. Learning C++. McGraw-Hill, 1991 [4] DEITEL, H.M.; DEITEL, P.J. Java: Como Programar. [5] MONTENEGRO, Fernando; PACHECO, Roberto. Orientação Objetos em C++. Editora Ciência Moderna, 1994. [6] BUENO, André. Apostila de Programação em C++, 2002. [7] LEITÃO, Helena. http://www.dei.isep.ipp.pt/ hleitao/ [8] BITENCOURT, Tatyana. Grupo de Usuários Java http://www.guj.com.br [9] CESTA, André. TUTORIAL: ”A LINGUAGEM DE PROGRAMAÇÃO JAVA”, 1996, http://www.dcc.unicamp.br/ aacesta