Aprendendo Java 2 Rodrigo Mello Ramon Chiara Renato Villela Novatec Editora Ltda. www.novateceditora.com.br 1 Programação Orientada a Objetos O que é Programação Orientada a Objetos? É um paradigma de programação baseado no conceito de classes e objetos. As classes são elementos em que dados e procedimentos podem ser agrupados, segundo sua função para um determinado sistema; essas classes são codificações no formato de arquivos. Quando uma dessas classes é utilizada como um tipo de dado para a criação de uma variável, esta é chamada de objeto. O que é Classe? A classe é definida como uma estrutura de dados que contém métodos e atributos. No paradigma da orientação a objetos, os procedimentos ou funções (presentes em linguagens estruturadas, tais como C e Pascal) são chamados de métodos de uma classe. As variáveis, que são declaradas dentro de uma classe, são chamadas de atributos. Ao se criar uma classe, o objetivo é agrupar métodos e atributos que estejam relacionados entre si. Uma classe é composta de partes e estas devem representar alguma funcionalidade segundo o objetivo da classe. Por exemplo, uma classe Cliente pode ter atributos como nome_cliente, endereco_cliente e outros dados relacionados ao cliente. Exemplos dos métodos que atuam sobre tais dados podem ser inserir_cliente, excluir_cliente e alterar_cliente. 13 Capítulo 1: Programação Orientada a Objetos As partes que compõem a classe Cliente devem representar funcionalidades segundo o objetivo dessa classe, ou seja: 1. Quais os dados relacionados a um cliente (nome, endereço, cidade, estado, país etc.). 2. Quais outras informações desse cliente devem ser armazenadas? 3. Quais atitudes um cliente pode tomar ou quais atitudes podem ser tomadas diante de um cliente (estas, geralmente, se tornam métodos da classe)? Enfim, a classe é o código que declara atributos e métodos, em que cada um destes possa fazer parte da representação de um mesmo objetivo. O objetivo da classe é representar de forma adequada uma entidade (tal como um cliente, fornecedor etc.) dentro de um sistema. Exemplo de uma classe em Java // declaração inicial da classe Cliente public class cliente { // atributos da classe Cliente String nome; String endereco; // métodos da classe Cliente public void setNome (String novo_nome) { nome = novo_nome; } public void setEndereco (String novo_endereco) { endereco = novo_endereco; } public String getNome() { return nome; } public String getEndereco() { return endereco; } } // fim da declaração da classe Cliente 14 Capítulo 1: Programação Orientada a Objetos A classe Cliente apresenta atributos, que são utilizados para armazenar informações relacionadas ao cliente, e métodos, que representam ações tomadas sobre os dados contidos em um objeto dessa classe ou objetos de outras classes. Os métodos definidos na classe Cliente recuperam e alteram somente os dados contidos em um objeto dessa própria classe. O que é Objeto? Enquanto uma classe é somente a codificação na forma de um arquivo texto, um objeto é uma instância de uma classe, ou seja, é uma porção de memória reservada para armazenar os dados e os métodos declarados na classe. Enfim, um objeto é a instanciação de uma classe na memória. A classe é o código-fonte escrito em um arquivo texto, enquanto o objeto é uma parte de uma aplicação durante o processo de execução. No objeto, pode-se executar os métodos que foram definidos, além de acessar e alterar dados. class X { ... } Disco Rígido Memória Figur a1 .1 – Objeto do tipo Cliente e sua classe correspondente. igura 1.1 O que é Mensagem? A mensagem é definida como o ato de chamar ou requisitar a execução de um método. Portanto um objeto do tipo Cliente apresenta diversos métodos; o ato de requisitar a execução de um desses métodos é conhecido como mensagem. Um objeto pode enviar mensagens para outros objetos requisitando a execução de métodos. 15 Capítulo 1: Programação Orientada a Objetos O que é Encapsulamento? É a capacidade de restringir o acesso a elementos de uma classe utilizando qualificadores. Um qualificador é uma palavra reservada, que define a visibilidade de determinado atributo ou método, em uma linguagem orientada a objetos. Por exemplo, na linguagem C++ existem os qualificadores private, public e protected, e na linguagem Java existe, ainda, mais um qualificador, conhecido como package. A função dos qualificadores é definir o acesso aos dados e métodos de uma classe. O qualificador public, por exemplo, permite que determinado método ou atributo seja acessado diretamente por qualquer outro objeto. Observe a seguir a tabela 1.1 com as características dos qualificadores na linguagem C++ e Java: Qualificadores Java private O método ou atributo pode ser acessado somente dentro da própria classe. public O método ou atributo pode ser acessado externamente por outro código. protected O método ou atributo pode ser acessado pela própria classe ou por classes-filhas (aquelas que herdam desta classe). package (sem modificador) O método ou atributo pode ser acessado pela própria classe ou por classes que participem do mesmo pacote. C++ O método ou atributo pode ser acessado somente dentro da própria classe. O método ou atributo pode ser acessado externamente por outro código. O método ou atributo pode ser acessado pela própria classe ou por classes-filhas (aquelas que herdam desta classe). Não existe em C++. Tabela 1 .1 – Tabela com os qualificadores nas linguagens Java e C++. 1.1 Métodos Atributos Figur a1 .2 – Encapsulamento em uma classe. igura 1.2 16 Capítulo 1: Programação Orientada a Objetos A seguir, um exemplo de uma classe em Java que utiliza qualificadores: // declaração inicial da classe Clientes public class Clientes { // atributos da classe Clientes - estes podem ser acessados somente dentro da classe private String nome; private String endereco; // atributos da classe Clientes acessíveis por meio de código externo à classe public String cidade; public int idade; // atributos da classe Clientes acessíveis por meio de código externo à classe, // desde que pertença ao mesmo pacote dessa classe existe em Java, // não existe esse tipo de declaração em C++ float saldo; String rg; // métodos da classe Clientes public void setNome (String novo_nome) { nome = novo_nome; } public void setEndereco (String novo_endereco) { endereco = novo_endereco; } public String getNome() { return nome; } public String getEndereco() { return endereco; } } // fim da declaração da classe Clientes 17 Capítulo 1: Programação Orientada a Objetos O que é Herança? A herança torna possível a reutilização de funcionalidades previamente definidas em uma classe. A finalidade é que a subclasse (aquela que herda as características de determinada classe) inclua o comportamento da superclasse (aquela que contém o comportamento a ser herdado) e adicione mais funcionalidades. Não seria interessante a herança se não houvesse a possibilidade de adicionar funcionalidades à subclasse. A figura 1.3 representa uma subclasse que herda características da superclasse e lhe adiciona alguma funcionalidade. Figura 1.3 – Mecanismo da Herança. Ao processo de definição de subclasses, em que uma classe herda características de outra e assim sucessivamente, dá-se o nome de hierarquia de classes. Essa hierarquia de classes segue uma apresentação na forma de árvore, em que a raiz é a superclasse de todo o sistema. Cada nível abaixo dessa superclasse do sistema acrescenta funções específicas. Figura 1.4 – Exemplo de Hierarquia de Classes. 18 Capítulo 1: Programação Orientada a Objetos Na figura 1.4 é apresentada uma hierarquia de classes, em que a classe Funcionario representa a superclasse do sistema. No nível abaixo dessa superclasse existem especializações da superclasse, ou seja, subclasses que herdam as características da classe Funcionario e nas quais podem ser adicionadas características específicas. Na figura 1.4 existem as subclasses FuncionarioContratado e FuncionarioTemporario. Pode-se relacionar subclasses à superclasse questionando se a subclasse "é uma" superclasse. Por exemplo, pense na seguinte questão: "um FuncionarioContratado é um Funcionario?". A resposta é sim. Caso a resposta seja negativa, a herança provavelmente esteja incorreta ou durante a fase de projeto do sistema foi necessária a criação de tal estrutura. Figura 1.5 – Exemplo de hierarquia errada. Existem construções de hierarquia de classes que apresentam erros. A figura 1.5 apresenta um exemplo de hierarquia em que a superclasse Carro tem as subclasses Porta e Pneu. Essa técnica não expressa corretamente o conceito de hierarquia, em que as subclasses especializam, ou seja, afunilam o conceito da superclasse. Caso se utilize o mesma linha de raciocínio realizada anteriormente para essa hierarquia, tal como: "Uma Porta é um Carro?", a resposta será negativa. Portanto o correto é definir uma subclasse que se relacione com a superclasse, em que a subclasse seja o conceito da superclasse adicionando-se algum conteúdo que possa representar de forma mais específica o conceito apresentado pela superclasse. A herança pode ser múltipla em que uma subclasse herda características de diversas superclasses. A linguagem C++ suporta esse tipo de herança, enquanto a linguagem Java suporta somente a herança de uma única classe. 19 Capítulo 1: Programação Orientada a Objetos Figura 1.6 – Mecanismo da Herança Múltipla. A seguir, um exemplo de herança em Java: /*———declaração da superclasse———*/ class madeira { // atributo da classe madeira String cor; // métodos da classe madeira public String getCor() { return cor; } public void setCor(String nova_cor) { cor = nova_cor; } } // fim da declaração da superclasse /*————declaração da subclasse————*/ class cadeira extends madeira { // atributos adicionais à superclasse int peso; // métodos adicionais à superclasse public void escreve_tela() { System.out.println(this.getCor()); } } // fim da declaração da subclasse 20 Capítulo 1: Programação Orientada a Objetos O que é Sobrecarga de Métodos e Operadores? Sobrecarregar um método ou operador é permitir que se utilize o mesmo nome de chamada para operar sobre uma variedade de tipos de dados. Pode-se ter diversos métodos declarados com o mesmo nome dentro de uma classe, entretanto tais métodos não podem ter os mesmos tipos de parâmetros seguindo a ordem de declaração. Por exemplo, pode-se ter o método setValor(float novo_x) e setValor(float novo_x, float novo_y) em que o tipo do primeiro parâmetro é o mesmo, mas existe um segundo parâmetro em um dos métodos que diferencia as duas declarações. Errado, portanto, seria declarar o método setValor(float novo_x) e setValor(float novo_y), pois o tipo do parâmetro é idêntico e o número de parâmetros também, não havendo diferenças entre as duas declarações. Para sobrecarregar métodos, deve-se, inicialmente, detectar quais métodos serão sobrecarregados. Nunca defina métodos que apresentem a mesma ordem de declaração de parâmetros, tendo estes o mesmo tipo de dados e estando em número idêntico. Como saber se é válida a sobrecarga de métodos? Método Caso coexistam em uma classe 1. setValor(float novo_x) O método tem uma variável do tipo float. 2. setValor(float novo_y) Caso coexistam os métodos 1 e 2 na mesma classe, o compilador apresentará erros, pois detectará dois métodos que recebem apenas uma variável do mesmo tipo. 3. setValor(float novo_x, float novo_y) Caso coexistam os métodos 1 e 3 ou os métodos 2 e 3, o exemplo funcionará corretamente. 4. setValor(float novo_x, int nova_variavel) Caso coexistam os métodos 1 e 4 ou 2 e 4, o exemplo funcionará corretamente. Caso os métodos 3 e 4 estejam na mesma classe, o exemplo também funcionará corretamente, pois apesar de o número de parâmetros ser igual, o tipo deles na ordem de declaração é diferente. Ou seja, pode-se ter o mesmo número de parâmetros quando o tipo deles na ordem de declaração do método é diferente (pois é assim que o compilador analisa para fazer a chamada ao método); no caso 3 tem-se (float, float) e no caso 4 temse (float, int). Existe, ainda, como citado anteriormente, a sobrecarga de operadores. A linguagem Java suporta somente sobrecarga de métodos. Existem, contudo, linguagens, como C++, que suportam sobrecarga de métodos e operadores. 21 Capítulo 1: Programação Orientada a Objetos A seguir, um exemplo de classe em Java que tem o método setValor sobrecarregado: // declaração da classe vetor public class vetor { // atributos da classe vetor private float X; private float Y; // métodos da classe vetor // ocorre sobrecarga no método setValor, pois ele manipula // uma ou duas variáveis do tipo float public void setValor(float novo_x) { X = novo_x; } public void setValor(float novo_x, float novo_y) { X = novo_x; Y = novo_y; } } // fim da declaração da classe vetor A sobrecarga de operadores é oferecida para classes que desejem efetuar adição, subtração e outras operações. A seguir, um exemplo na linguagem C++ em que existe a sobrecarga do operador adição (+) para a manipulação de objetos do tipo Vetor2D. // declaração da classe em C++ class Vetor2D { // atributos float x, y; public: // construtor Vetor2D(Vetor2D v2); // métodos void EscreveVideo(char texto[]); Vetor2D operator + (Vetor2D v2); } // fim da classe Vetor2D 22 Capítulo 1: Programação Orientada a Objetos No exemplo anterior é declarado um método: Vetor2D operator + (Vetor2D v2); Existe nesse método a sobrecarga do operador + (adição). Essa sobrecarga torna possível a soma de dois objetos do tipo Vetor2D: Vetor2D v1 = new Vetor2D; Vetor2D v2 = new Vetor2D; // uso do operador adição entre instâncias do tipo Vetor2D Vetor2D v3 = v2 + v1; No caso apresentado acima, o objeto v2 é responsável por executar de forma implícita uma chamada para o operador adição sobrecarregado (+), o qual executa uma determinada função sobre os dados. Na chamada para execução do operador sobrecarregado, é enviado o objeto v1 como parâmetro. Na declaração da classe Vetor2D, observa-se que o operador adição sobrecarregado recebe somente um parâmetro que é do mesmo tipo da classe. O que é Polimorfismo? É a troca de mensagens entre métodos de uma subclasse com sua superclasse e assim sucessivamente. O polimorfismo pode ser observado por meio de métodos virtuais de uma classe como representado a seguir: Figura 1.7 – Métodos Virtuais. Na figura acima observa-se que o método M1 da classe A chama o método M2 da mesma classe. Caso seja criada uma classe B que seja subclasse da classe A e seja reescrito o método M2, têm-se: 1. Quando o método M1 for chamado por meio de um objeto da classe B, então o método M1 é procurado nesta e não é encontrado, procura-se o método nas superclasses da classe B até ser encontrado. 23 Capítulo 1: Programação Orientada a Objetos 2. Após o método M1 ser chamado, ele chama o método M2 que é, então, procurado desde a subclasse, onde foi originada a mensagem, passando às superclasses da hierarquia até ser encontrado. 3. Caso o método M2 seja encontrado na classe B (que enviou uma mensagem para M1), então o método a ser chamado é o M2 da classe A ou o M2 da classe B? A resposta correta é o M2 da classe B, pois ele foi reescrito na classe que está fazendo chamadas para o método M1. Para entender melhor o funcionamento do polimorfismo, considere uma classe Madeira como superclasse, e uma subclasse chamada Cadeira. Considere o método escreve_na_tela na classe Madeira, o qual faz uma chamada para o método escreve na mesma classe. Caso o método escreve seja redefinido na subclasse, ou seja, na classe Cadeira, se ocorrer uma chamada do método escreve_na_tela por meio de um objeto da classe Cadeira, que originou a mensagem, o método a ser chamado por escreve_na_tela será o escreve da classe Cadeira. Desta forma, partindo de um objeto do tipo Cadeira toda mensagem executada, ou seja, uma chamada a um método gera uma busca do método nesta classe, e caso não seja encontrado, é feita uma busca na superclasse da classe Cadeira, ou seja, a classe Madeira. Caso não seja encontrado na classe Madeira, ocorre uma busca na superclasse da classe Madeira e assim sucessivamente, até chegar à raiz da hierarquia de classes. Encontrando o método, ele é executado e qualquer outra chamada dentro dele gera novamente uma busca por um método desde a classe Cadeira; caso não seja encontrado, será feita um busca na superclasse da classe Cadeira e assim sucessivamente. Considere o exemplo em Java a seguir: /*———declaração da superclasse———*/ class Madeira { // atributo da classe Madeira String cor; // métodos da classe Madeira public void escreve_na_tela() { escreve(); } public void escreve() { System.out.println("estou na classe Madeira!"); } } // fim da declaração da superclasse 24 Capítulo 1: Programação Orientada a Objetos /*——————declaração da subclasse——————*/ class Cadeira extends Madeira { // métodos adicionais à superclasse public void escreve() { System.out.println("estou na classe Cadeira!"); } } // fim da declaração da subclasse Caso se declare um objeto da classe Madeira e execute o método escreve_na_tela, este faz uma chamada a escreve da classe Madeira retornando o texto: estou na classe Madeira! Caso se declare um objeto da classe Cadeira que chame o método escreve_na_tela, este faz uma chamada ao método escreve da classe em que o objeto foi declarado, ou seja, classe Cadeira, e caso o método não seja encontrado, a chamada será repassada à superclasse da classe Cadeira e assim sucessivamente. Como o método escreve foi encontrado na classe Cadeira esse é executado e gera a saída: estou na classe Cadeira! Na prática, cada classe procura os métodos sempre da classe de onde está fazendo as chamadas em direção às superclasses, por exemplo: Figura 1.8 – Busca de métodos virtuais. 25 Capítulo 1: Programação Orientada a Objetos Na figura 1.8, o método M1 chama o método M2 na classe A. Considere a declaração da classe B que herda as características da classe A e nesta seja reescrito o método M2, declarando um objeto do tipo classe B que executa o método M1, o que ocorre? 1. Primeiro, como se sabe, os métodos são procurados a partir de um objeto de uma determinada classe em direção às superclasses. 2. Como o método M1 não existe na subclasse B, ele é procurado nas superclasses até ser encontrado na classe A. 3. Após ser encontrado, ele é executado e chama o método M2. Quando essa chamada ocorre, o compilador procura o método M2 a partir do tipo do objeto que executou a primeira chamada, ou seja, um objeto do tipo B. 4. A procura do método M2 inicia-se a partir da classe B, onde já é encontrado e chamado. 5. O conceito de polimorfismo é a ordem em que os métodos são montados para sua chamada, da subclasse para a superclasse até ser encontrado. Na linguagem Java todos os métodos são virtuais, portanto suportam polimorfismo, bastando ser redeclarados nas subclasses. Em C++ isso não ocorre, a superclasse deve permitir que determinado método seja reescrito nas subclasses. O que são Construtores e Destrutores? Construtores são métodos especiais chamados no processo de instanciação de um objeto no sistema. A execução de tais métodos garante a inicialização dos identificadores de forma correta. Todo construtor tem o mesmo nome da classe. O exemplo a seguir apresenta dois construtores, um que não recebe parâmetro algum e outro que recebe o nome de um cliente: // declaração inicial da classe ciente public class cliente { // atributos da classe cliente private String nome; private String endereco; 26 Capítulo 1: Programação Orientada a Objetos // construtores public cliente() { nome = ""; endereco = ""; } public cliente(String novo_nome) { nome = novo_nome; endereco = ""; } // métodos da classe cliente public void setNome (String novo_nome) { nome = novo_nome; } public void setEndereco (String novo_endereco) { endereco = novo_endereco; } public String getNome() { return nome; } public String getEndereco() { return endereco; } } // fim da declaração da classe Cliente Todo construtor recebe o mesmo nome da classe. No exemplo anterior existem dois construtores sobrecarregados, um que não recebe nenhum parâmetro e outro que recebe uma string novo_nome. Portanto a criação de um objeto da classe cliente pode ser feita de duas formas, pois existem dois construtores: Primeira Forma // utilizando o construtor sem parâmetros que inicia o objeto do tipo cliente. // É executado o código contido nesse construtor cliente c1 = new cliente(); 27 Capítulo 1: Programação Orientada a Objetos Segunda Forma // utilizando o construtor que recebe como parâmetro uma string que identifica // o nome do cliente. É executado o código contido nesse construtor cliente c2 = new cliente("o cliente"); O construtor é chamado somente durante o processo de instanciação do objeto. Nos construtores são definidos comportamentos de iniciação do objeto. Quando um objeto, por exemplo, do tipo Cliente é instanciado, espera-se que o comportamento inicial do cliente seja satisfeito pelos construtores. Quais tipos de comportamento são esperados na instanciação de um cliente? Existem parâmetros como nome do cliente, endereço, cidade, estado etc. Por meio dessa análise de comportamento são implementados os construtores. Destrutores são métodos especiais chamados na finalização do objeto. Quando as posições de memória que um objeto ocupa devem ser liberadas, o destrutor é chamado. Na linguagem C++, todo destrutor tem o nome da classe precedido por ~ (til). Observe o exemplo em C++ a seguir: // declaração da classe em C++ class Vetor2D { // atributos float x, y; public: // construtor Vetor2D(Vetor2D v2); // destrutor ~Vetor2D(); // métodos void EscreveVideo(char texto[]); Vetor2D operator+ (Vetor2D v2); } // fim da classe Vetor2D 28 Capítulo 1: Programação Orientada a Objetos Quando um objeto do tipo Vetor2D estiver sendo removido da memória, o destrutor é chamado e nele deve haver chamadas que liberem a memória que está sendo utilizada pelo objeto. Quando uma instância é criada utilizando a palavra new, ela é alocada na memória heap. As demais instâncias, tais como int, long entre outras, são alocadas na stack (pilha do sistema). A stack é liberada automaticamente quando um programa ou objeto morre, entretanto a memória heap não é liberada automaticamente. Portanto, deve-se liberar a memória que foi alocada na heap utilizando a chamada delete sobre as instâncias que foram declaradas com o uso da palavra reservada new. Por exemplo: // para instanciar cliente c1 = new cliente(); // para liberar a memória heap delete c1; Figura 1.9 – A memória heap e stack. Portanto, o objetivo do destrutor é liberar a memória heap, alocada usando new, que não é liberada automaticamente. A stack é liberada automaticamente. 29