Programação Orientada por Objectos 1 História da linguagem Java Os microprocessadores revolucionaram a indústria dos computadores. Pensa-se que a próxima área na qual os microprocessadores terão um profundo impacto, será nos dispositivos electrónicos de consumo inteligentes. Por essa razão, a empresa Sun Microsystems financiou em 1991, um projecto de investigação, designado “Green project”, cujo objectivo era desenvolver software para controlar dispositivos electrónicos de consumo tais como electrodomésticos, televisões interactivas, sistemas de iluminação, etc, tornando-os inteligentes e permitindo a comunicação entre eles. O protótipo desenvolvido designou-se por “star7”, podia ser controlado à distância e comunicar com outros do mesmo tipo. O projecto usava a linguagem C++, o que se tornou muito complicado. Um dos membros, James Gosling, criou uma nova linguagem para o star7, a que chamou “oak”, porque do gabinete dele via essa árvore. Os programas deviam ser pequenos, fiáveis, eficientes e facilmente portáveis entre diferentes dispositivos de hardware. O projecto foi apresentado a representantes de electrónica de consumo que não o aceitaram, porque o mercado para os dispositivos electrónicos de consumo não desenvolveu tão rapidamente como a Sun antecipou. O projecto esteve para ser cancelado mas em 1993 a World Wide Web ganha muita popularidade e o grupo de investigação viu o potencial de usar a linguagem para criar páginas dinâmicas. Os programas eram pequenos, facilmente carregados, seguros e portáveis, para além de ser uma linguagem de uso genérico que pode correr em diferentes plataformas. Para demonstrar o potencial da linguagem, construíram em 1994, com a própria linguagem um browser, que podia correr applets, e designaram-no por WebRunner. Em Janeiro de 1995, pressionados para lançar o produto, verificaram que “oak” era uma marca registada da “Oak Technologies” e “WebRunner” da “Taligent”. Fizeram uma reunião do tipo “brainstorm”com todos os elementos do grupo de investigação designado por “Live Oak” para escolherem um novo nome para a linguagem. Surgiram muitos nomes, um dos quais Java por um dos elementos estar a beber café Pete’s Java, e este foi o escolhido para a linguagem e HotJava para o browser. Em Maio de 1995 a Sun anuncia formalmente a linguagem Java, mas esta linguagem só alcançou muita popularidade depois da Netscape a licenciar em Agosto de 1995. A revista Time designou Java um dos dez melhores produtos de 1995. Desenvolvimento de programas: Software de desenvolvimento da Sun: http://java.sun.com Java Development Kit (JDK), gratuito. NetBeans (ambiente de desenvolvimento integrado – IDE), gratuito. BlueJ: http://www.bluej.org DEI - ISEP gratuito. Fernando Mouta 2 Programação Orientada por Objectos O Modelo de Compilação Quando se compila um programa escrito em C ou noutras linguagens o compilador traduz o código fonte em código máquina – instruções específicas do processador do computador. O programa compilado só correrá em computadores com esse processador. Se se pretender usar o mesmo programa noutra plataforma é necessário transferir o código fonte para a nova plataforma e recompilá-lo para produzir código específico para esse sistema. Por vezes é necessário fazer pequenas alterações. Compiladores tradicionais: Código fonte --> Compilador para um dado CPU -- > Código específico para um dado CPU O processo de compilação de programas em Java usa uma nova concepção. Bytecodes – conjunto de bytes formatados (para uma máquina virtual Java). Compilação de programas em Java: Código fonte --> Compilador -- > Bytecodes (conjunto de bytes formatados) --> Interpretador para um dado CPU --> Código específico para um dado CPU Características: Execução mais lenta, Grande portabilidade do código em bytecodes, desde que um computador disponha de interpretador Java. A linguagem Java permite criar 2 tipos de programas: • Applets: programas escritos em Java e compilados para ser executados num browser ou num emulador de browser - appletviewer. • Aplicações: programas escritos em Java e compilados que correm sozinhos. Classes e objectos Os programas em Java são construídos a partir de classes. Depois de efectuada a definição de uma classe pode-se criar qualquer número de objectos. Criar um objecto a partir da definição de uma classe designa-se por instanciação, daí que os objectos são muitas vezes designados por instâncias. Cada classe pode conter 3 tipos de membros: campos de dados, métodos e classes ou interfaces. Embora classes ou interfaces possam ser membros de outras classes ou interfaces, vamos começar por considerar só os dois primeiros tipos de membros: campos de dados e métodos. Campos de dados são dados que pertencem ou à própria classe ou a objectos da classe. Eles representam o estado do objecto ou da classe. São implementados por variáveis que vão conter informação relativa aos objectos da classe. Métodos são conjuntos de instruções que operam nos campos de dados para manipular o estado do objecto ou da classe. Contêm o código correspondente às acções que os objectos da classe podem executar. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 3 Definição de uma classe: class NomeDaClasse { camposDeDados métodos } Definição de uma classe como sendo uma subclasse de outra: class NomeDaClasse extends NomeDaSuperclasse { camposDeDados métodos } Declaração de uma classe que representa um ponto num plano a 2 dimensões: class Ponto { double x, y; public void limpar() { x = 0; y = 0; } } A classe Ponto tem 2 campos de dados representando as coordenadas x e y de um ponto, e 1 método limpar() que coloca x e y (do objecto ao qual é aplicado) a zero. Extensão da classe Ponto para representar um pixel que possa ser mostrado no ecrã (Pixel estende dados e comportamento da classe Ponto): class Pixel extends Ponto { Color cor; public void limpar() { super.limpar(); cor = null; } } Criação de objectos: Os objectos são criados usando uma expressão contendo a palavra-chave new. Em Java os objectos criados são alocados numa zona de memória designada heap. Ponto p1 = new Ponto(); Ponto p2 = new Ponto(); p1.x = 10.0; p1.y = 20.0; p1.limpar(); // invocação de um método Em geral pretende-se que um campo de dados (atributo) num objecto seja diferente do campo de dados com o mesmo nome em qualquer outro objecto da mesma classe (e por isso esses atributos implementam-se como variáveis de instância). Mas por vezes pretendemos campos de dados de dados que sejam partilhados por todos os objectos da classe – implementam-se com variáveis estáticas ou de classe. public static double xMax = 10.0; public static double yMax = 10.0; DEI - ISEP Fernando Mouta 4 Programação Orientada por Objectos Exemplo de uma aplicação Aplicação (programa HelloWorld para construir uma aplicação: Hello.java): public class Hello { public static void main (String args []) { System.out.println(“Hello World !”); } } para compilar: para correr o programa: javac Hello.java java Hello O programa declara uma classe com o nome Hello a qual não contém campos de dados e tem um único método com o nome main. O método main recebe um parâmetro que é um array de objectos String que são os argumentos com os quais o programa pode ser invocado da linha de comandos.O método main é declarado void porque não retorna qualquer valor. Neste exemplo, o método main contém uma única instrução que invoca o método println() no objecto out da classe System. Exemplo de uma applet HelloApplet.java: import java.applet.Applet; import java.awt.Graphics; public class HelloApplet extends Applet { public void paint (Graphics g) { g.drawString(“Hello World !”, 50, 20); } } Os browsers mostram o conteúdo de documentos que contêm texto. Para executar um applet Java num browser, deve-se fornecer um ficheiro de texto HTML com a indicação do applet que o browser deve carregar e executar. HelloApplet.html: <html> <applet code=“HelloApplet.class” width=200 height=50> </applet> </html> O ficheiro HTML diz ao browser o applet específico a carregar e o tamanho da área na qual o applet vai ser mostrado. O browser cria um objecto dessa classe e para esse objecto invoca determinados métodos numa determinada ordem. Um dos métodos mais importantes é o método paint, o qual é chamado automaticamente pelo browser quando executa o applet. O método paint desenha gráficos (linhas, rectângulos, ovais e strings de caracteres) no ecrã. A única maneira de executar um applet é a partir de um documento HTML (ou através de um emulador de browser – appletviewer). Fernando Mouta DEI - ISEP Programação Orientada por Objectos 5 Classes e Objectos Java – linguagem de programação orientada por objectos. A programação orientada por objectos é uma forma de organização lógica de programas através de um nível de abstracção adequado para modelizar as entidades de um modo semelhante ao modo como se relacionam no mundo real. Trata-se de uma simulação do mundo real conceptualizado como um sistema de objectos em interacção. As entidades reais são modelizadas através de objectos e classes. Classes – são definições de características comuns a um conjunto de objectos. As características dos objectos podem-se exprimir através de: Atributos e Comportamentos. • Os atributos dos objectos pertencentes a uma classe implementam-se através de variáveis. • Os comportamentos dos objectos pertencentes a uma classe implementam-se através de métodos que são funções que podem receber (ou não) parâmetros, realizam uma determinada acção e podem retornar (ou não) resultados. Criar um objecto a partir de uma classe é instanciar a classe. Um objecto é uma instância de uma classe. Todo o objecto em Java pertence a uma classe que define os seus dados e comportamento. Cada classe pode ter 3 tipos de membros: • Campos de dados – são variáveis de dados associadas à classe e aos objectos. Os campos de dados também servem para armazenar resultados de cálculos realizados pelos métodos da classe. • Métodos – são conjuntos de instruções que contêm o código executável de uma classe. • Classes e interfaces. Introdução à Programação Orientada por Objectos em Java De um modo semelhante a muitas actividades económicas, em que um produto é fabricado através da integração de vários componentes seleccionados e adquiridos, também em programação a utilização de módulos integráveis com características e funcionalidades independentes do contexto, torna mais económica e de melhor qualidade a produção de software assim como a sua manutenção. Um programa realizado segundo o paradigma da Programação Orientada por Objectos (POO) é constituído por objectos, possuidores de características específicas e capazes de realizar determinadas operações. Cada objecto é responsável por realizar um conjunto de tarefas. Se uma tarefa depende de outra que não é da sua responsabilidade tem de ter acesso a um outro objecto capaz de a realizar. No entanto um objecto nunca deve manipular directamente os dados internos de outro objecto, mas apenas comunicar através de mensagens. Deste modo promove-se a reutilização de código, reduz-se a dependência dos dados e facilita-se a detecção de erros, características importantes da POO. DEI - ISEP Fernando Mouta 6 Programação Orientada por Objectos Agregando dados e comportamentos num único módulo esconde-se do utilizador a implementação dos dados atrás das operações neles realizadas. Assim, separa-se a estrutura e mecanismos internos do programa, da interface utilizada pelo programador cliente, o que facilita a compreensão e uso na reutilização de código, representando outra vantagem do paradigma da POO. A reutilização do código pode ser efectuada por 2 processos diferentes: Por composição, se o objecto a construir é composto por outros objectos, permitindo utilizar as funcionalidades dos seus componentes; Por herança, se o objecto a construir deve ter a estrutura e o comportamento de outro, mas com algumas adições ou modificações, correspondendo a uma especialização ou derivação. A herança possibilita assim a programação incremental que facilita a extensão dos programas assim como a programação genérica através do polimorfismo, pois código que funciona com um tipo genérico também funciona com novos subtipos que se criem. Estas são as outras características importantes da POO. A POO é um modelo de concepção e desenvolvimento de programas que aproxima a programação do modo como os humanos representam e operam os conceitos e objectos do mundo, facilitando o desenvolvimento de aplicações reais. A POO baseia-se num pequeno número de conceitos fundamentais do senso-comum, isto é, em ideias simples. Os quatro princípios chave em que se baseia a POO são: abstracção, encapsulamento, herança e polimorfismo. Vamos descrever estes conceitos em termos de exemplos do mundo real e das correspondentes abstracções de programação. Abstracção Abstracção é o processo de extracção das características essenciais de um conceito ou entidade no mundo real para o poder processar num computador. As características que escolhemos para representar o conceito ou entidade dependem do que se pretende fazer. Estas características fornecem um tipo de dados abstracto. Tomando como exemplo um automóvel, para o registo, as características importantes são o número de identificação do veículo atribuído pelo fabricante, a matrícula, e o dono, enquanto que para a reparação numa garagem, as características essenciais seriam a matrícula, o dono, a descrição do serviço a efectuar e a factura. A abstracção é o processo de determinar quais são as características apropriadas dos dados excluindo os detalhes não importantes para a resolução do problema, constituindo essas características um tipo de dados abstracto. Encapsulamento Todas as linguagens têm uma maneira de armazenar pedaços de informação relacionada juntos, normalmente designados estrutura ou registo. Na programação orientada por objectos reconhece-se que tão importante como os dados são as operações que neles são realizadas. O encapsulamento consiste na agregação dos dados e seus comportamentos numa única unidade organizacional. Em termos de linguagem de programação isto significa que os dados e as funções que os manipulam devem ser agrupados num tipo de dados, de modo que se possa determinar as características de objectos desse tipo assim como as únicas operações que neles se possam realizar. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 7 As linguagens não orientadas por objectos já suportam encapsulamento para tipos de dados primitivos ou internos, tais como inteiro (“int”) ou real (“float”), para os quais existem um conjunto bem definido de operações que se podem aplicar. As linguagens orientadas por objectos suportam o encapsulamento também para tipos definidos pelo utilizador. Vamos mostrar exemplos, partindo da linguagem de programação C (muito popular, mas não orientada por objectos). Vamos supor que pretendíamos representar circunferências num plano a duas dimensões. As características que abstraíamos eram as coordenadas do centro e o raio, as quais podiam ser armazenadas numa estrutura como a seguinte: typedef struct { float x; float y; float raio; } Circunferencia; Consideremos que tínhamos ainda uma função que calculava a área, dado um apontador para uma estrutura circunferência. float area( Circunferencia *c) { return 3.14 * (c->raio) * (c->raio); } A estrutura Circunferencia tem três campos que são “float” para armazenar a coordenada x, a coordenada y, e o valor do raio. A função “area” tem um parâmetro designado “c” que é um apontador para uma variável Circunferencia. No corpo da função multiplica-se 3.14 pelo quadrado do valor do campo raio da variável apontada por “c”, e retorna-se o resultado como valor da função. A invocação desta função poderia ser feita como a seguir: float a; Circunferencia c1; c1.x = 5.0; c1.y = 6.0; c1.raio = 3.0; a = area( &c1 ); A função “area” recebe um apontador para uma variável Circunferencia (e não uma variável Circunferencia), por isso é-lhe passado o endereço da variável “c1”. Num programa em C poderíamos ter outras definições de estruturas e de funções, todas ao mesmo nível. A função “area” e a estrutura de dados sobre a qual a função opera não estão agregadas numa unidade organizacional. Seria possível a um programador que usasse este programa adicionar mais funções que operassem sobre esta estrutura de dados, assim como aceder directamente aos campos da estrutura e alterá-los. Para fazermos a transição para a programação orientada por objectos, vamos começar por agrupar a definição da estrutura com todas as funções que operam sobre esta estrutura. Como isto não é possível na linguagem de programação C, vamos designar o seguinte código por pseudo-código C: DEI - ISEP Fernando Mouta 8 Programação Orientada por Objectos struct Circunferencia { float x; float y; float raio; float area( Circunferencia *c) { return 3.14 * (c->raio) * (c->raio); } }; Vamos impor agora a seguinte convenção: todas as funções que operam sobre uma variável do tipo de dados Circunferencia recebem um apontador para essa variável como primeiro argumento; a variável apontada pelo primeiro argumento será a variável sobre a qual a função realiza a operação. Com esta convenção poderemos retirar o apontador para a variável sobre a qual a função opera, quer da lista de parâmetros, quer antes dos campos para os quais aponta (no corpo da função) considerando que existe implicitamente. A definição do tipo de dados Circunferencia fica então: struct Circunferencia { float x; float y; float raio; float area( ) { return 3.14 * raio * raio; } }; Assim chegamos à definição de uma classe na linguagem orientada por objectos C++, bastando substituir a palavra struct por class: class Circunferencia { float x; float y; float raio; float area( ) { return 3.14 * raio * raio; } }; Na programação orientada por objectos as funções no interior de classes designam-se por métodos, no sentido de que definem o método (o plano, o esquema, as acções) para fazer qualquer coisa. A invocação do método para calcular a área de uma circunferência poderia ser feita como a seguir: Circunferencia c1; c1.x = 5.0; c1.y = 6.0; c1.raio = 3.0; float a = c1.area(); Fernando Mouta DEI - ISEP Programação Orientada por Objectos 9 Na execução do método “area” aplicado ao objecto c1, o campo de dados raio referido no corpo do método é o campo de dados da variável c1. A linguagem de programação é que se encarrega de, ao invocar o método “area”, enviar um apontador para a variável “c1” como primeiro parâmetro implícito, e usar esse apontador para descobrir os campos de dados referenciados no corpo do método. Deste modo os campos de dados são implicitamente reconhecidos dentro dos métodos, sem ter que dizer a que estruturas pertencem. A linguagem de programação Java apresenta uma diferença importante relativamente ao C++. Java trata as variáveis de dois modos diferentes, dependendo se são variáveis de tipos de dados internos ou primitivos, tais como int ou char, ou se são variáveis de tipos de dados definidos como classes. Em Java existem oito tipos de dados primitivos: byte, short, int, long, float, double, char, boolean. Quando se declara uma variável de um tipo de dados primitivo pode-se processá-la imediatamente. A declaração de uma variável de um tipo de dados primitivo corresponde à alocação de memória para essa variável (na stack, se é uma variável local, no heap se é uma variável de instância), e essa localização de memória pode ser usada imediatamente Quando se declara uma variável referência de objecto (variável de tipo de dados definido como classe) não se obtém uma instância (objecto) de uma dada classe e portanto não se pode ler, escrever ou processar essa instância. É necessário criar essa instância (objecto). Em Java a declaração “Circunferencia c1;” apenas aloca memória para uma variável de nome “c1” que pode referenciar um objecto do tipo Circunferencia, mas enquanto não for criado o objecto, e atribuído a c1 essa referência, não se pode aceder aos campos de dados do objecto referenciado por c1, porque ele não existe. Assim não se pode aceder a c1.raio, porque nem sequer existe memória alocada para os campos de dados do objecto Circunferência. Os objectos são criados utilizando a palavra-chave new seguida do tipo de objectos que se pretende criar e de parênteses. Os objectos criados são alocados numa área de memória designada heap. Todos os objectos são acedidos por referências a objectos, sendo esta a única maneira de acedermos aos objectos. Em geral há imprecisão na distinção entre os objectos e as referências a esses objectos. Uma variável referência de um objecto com o valor null não referencia qualquer objecto. Assim um objecto Circunferencia poderia ser criado assim: Circunferencia c1; // declaração da variável c1 = new Circunferencia(); // criação do objecto e retorno da // referência que é atribuída a c1 ou assim: Circunferencia c1 = new Circunferencia(); DEI - ISEP Fernando Mouta 10 Programação Orientada por Objectos O exemplo considerado, poderia ser assim codificado em Java: class Circunferencia { float x; float y; float raio; float area( ) { return 3.14 * raio * raio; } } A criação de um objecto, inicialização e invocação do método “area” poderia ser feita como a seguir: Circunferencia c1 = new Circunferencia(); c1.x = 5.0; c1.y = 6.0; c1.raio = 3.0; float a = c1.area(); Cada objecto Circunferência tem a sua própria cópia dos campos de dados x, y e raio. Se alterarmos o valor de um destes campos de dados num objecto, não alteramos o valor do campo com o mesmo nome noutro objecto. Estes campos de dados designam-se por variáveis de instância porque há uma cópia do campo para cada instância (objecto) da classe. Mas, às vezes, precisamos de campos de dados que sejam partilhados por todos os objectos da classe. Em Java obtêm-se estes campos de dados específicos da classe declarando-os static, pelo que normalmente se designam por campos de dados estáticos ou variáveis de classe (porque são específicos da classe e não aos objectos da classe). Haverá apenas uma cópia correspondente a um campo estático independentemente da quantidade de objectos criados, e mesmo que não seja criado nenhum. Um campo estático pode ser referenciado através da referência de qualquer objecto ou do nome da classe (preferencial). Construtores Para criar um objecto, é necessário declarar uma variável do tipo de uma classe e em seguida criar um novo (new) objecto dessa classe usando um construtor (dessa classe) conjuntamente com o operador "new". Ainda aos objectos criados é necessário dar um estado inicial. Esse estado inicial pode ser dado pela inicialização dos campos de dados expressos na própria declaração, mas muitas vezes é necessário realizar operações que não podem ser expressas como simples atribuições. Para isso usam-se os construtores. Os construtores têm o mesmo nome que a classe que inicializam, recebem zero ou mais parâmetros, mas não são métodos e não têm tipo de retorno. Pode-se associar o tipo de retomo ao nome do construtor (igual ao nome da classe). Fernando Mouta DEI - ISEP Programação Orientada por Objectos 11 Os construtores apresentam a seguinte estrutura: NomeDaClasse ( listaDeParametros ) { ... } Os construtores servem para colocar o objecto criado ( depois de alocada memória no heap para os campos de dados) num estado inicial. Os construtores são invocados depois das variáveis instância (dos objectos criados da classe) terem sido instanciados aos seus valores iniciais por omissão e depois da execução dos seus inicializadores explícitos (atribuições directamente aos campos de dados). Vejamos uma nova definição da classe Circunferencia com um construtor com parâmetros: class Circunferencia { float x; float y; float raio; Circunferencia( float x1, float y1, float raio1) { x = x1; y = y1; raio = raio1; } float area( ) { return 3.14 * raio * raio; } } A criação de um objecto e invocação do método “area” poderia agora ser feita assim: Circunferencia c1 = new Circunferencia(5.0, 6.0, 3.0); float a = c1.area(); Os argumentos dos construtores permitem fornecer parâmetros para a inicialização de um objecto. Suponhamos agora que pretendíamos atribuir um número único a cada objecto circunferência criado. Para isso incluímos mais um campo de dados do tipo “int” designado “numId” como variável de instância da classe Circunferencia. Cada objecto criado deverá ter um valor único para “numId”. Para que esta atribuição de um número único a cada objecto criado seja automática, necessitámos ainda de mais um campo de dados que contenha a quantidade de objectos criados até um dado momento. Vamos designar esse campo de dados por “proxNumId”. Não é necessário que em todos os objectos se aloque memória para um campo de dados “proxNumId”, basta que haja um para toda a classe, por isso vamos declará-lo como estático. DEI - ISEP Fernando Mouta 12 Programação Orientada por Objectos A classe Circunferencia fica agora com a seguinte definição: class Circunferencia { int numId; float x; float y; float raio; static int proxNumId=0; Circunferencia( float x1, float y1, float raio1) { numId = proxNumId++; x = x1; y = y1; raio = raio1; } float area( ) { return 3.14 * raio * raio; } } A criação de objectos continuaria a ser feita do modo definido anteriormente. Mas após ser criado um objecto, continua a ser possível aceder ao campo de dados numId e alterálo (não ficando garantida a existência de um número de identificação único para cada objecto) . Para não permitir que código fora da classe possa alterar um campo de dados, devemos declarar o campo de dados privado (com o modificador de acesso private). Membros de uma classe declarados “private” só podem ser acedidos por código dentro da própria classe. No entanto para o campo de dados ser útil e podermos utilizar (ler) o seu valor em código fora da classe, devemos acrescentar à classe um método público de acesso que retorne o valor do campo de dados. A definição da classe passaria a ser a seguinte. public class Circunferencia { private int numId; float x; float y; float raio; private static int proxNumId=0; public Circunferencia( float x1, float y1, float raio1) { numId = proxNumId++; x = x1; y = y1; raio = raio1; } public int getNumId() { return numId; } public float area( ) { return 3.14 * raio * raio; } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 13 Construtor por omissão ( “default constructor” ) A maioria das classes têm pelo menos um construtor. Se não é definido nenhum explicitamente, o compilador cria automaticamente um construtor por omissão - construtor sem argumentos (“no-arg constructor”). Por isso é normal declarar e inicializar objectos com chamadas do tipo: Bicicleta b1 = new Bicicleta(); Cerveja cheers = new Cerveja(); Um construtor não pode ser invocado explicitamente pelo programa a não ser na criação de um objecto, porque um construtor está associado à criação do objecto (embora pudesse ser útil invocá-lo após a criação para fazer o reset do objecto, isto é colocá-lo no estado inicial). Mas uma classe pode ter mais que um construtor, com diferentes assinaturas (isto é, diferentes listas de parâmetros, porque o nome é sempre o da classe). public class Circunferencia { private int numId; float x; float y; float raio; private static int proxNumId=0; public Circunferencia() { numId = proxNumId++; } public Circunferencia( float x1, float y1, float raio1) { this(); x = x1; y = y1; raio = raio1; } public int getNumId() { return numId; } public float area( ) { return 3.14 * raio * raio; } } Invocação explícita de um construtor A palavra chave this com uma lista de argumentos faz uma chamada explícita ao construtor da mesma classe com essa lista de argumentos. this() só pode ser usado para um construtor invocar outro construtor da mesma classe e tem de ser a primeira instrução executável. Agora temos duas maneiras de criar objectos: Circunferencia c1 = new Circunferencia(); Circunferencia c2 = new Circunferencia(7.0, 8.0, 5.0); DEI - ISEP Fernando Mouta 14 Programação Orientada por Objectos O construtor por omissão só é criado pelo compilador, se não se fornece nenhum construtor de qualquer tipo numa classe, e isto porque há classes para as quais um construtor sem argumentos seria incorrecto. Se se pretende ter, para além de um ou mais construtores com argumentos, um construtor sem argumentos, é necessário fornecê-lo explicitamente. O construtor por omissão é equivalente ao seguinte: public class Exemplo { public Exemplo() { } } A acessibilidade do construtor por omissão é igual à da classe. Integridade dos tipos de dados O encapsulamento permite reforçar a integridade dos tipos de dados, não permitindo aos programadores o acesso, de um modo inapropriado, aos campos de dados individuais. Consideremos o exemplo de uma classe que permita criar objectos representativos de um determinado instante de tempo caracterizados por três atributos: hora, minuto e segundo. A classe só deve permitir colocar os objectos com valores válidos de hora (0..23), minuto (0..59), e segundo (0..59). Para que os atributos de um objecto só possam ter valores válidos é necessário: declará-los como privados, para não poderem ser acedidos directamente de fora da classe, e providenciar código público que permita colocar valores nos campos de dados mas apenas valores válidos. A classe poderia ser definida do seguinte modo: public class Tempo { private int hora; // 0 - 23 private int minuto; // 0 - 59 private int segundo; // 0 - 59 public Tempo() { hora = 0; minuto = 0; segundo = 0; } public Tempo( int h, int m, int s ) { setHora( h ); setMinuto( m ); setSegundo( s ); } public void setHora( int h ) { hora = ( ( h >= 0 && h < 24 ) ? h : 0 ); } public void setMinuto( int m ) { minuto =( ( m >= 0 && m < 60 ) ? m : 0 ); } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 15 public void setSegundo( int s ) { segundo =( ( s >= 0 && s < 60 ) ? s : 0 ); } public int getHora() { return hora; } public int getMinuto() { return minuto; } public int getSegundo() { return segundo; } } Os campos de dados devem ser o mais privados possível, para manter a consistência dos dados. Os benefícios da programação orientada por objectos resultam de esconder a implementação da classe atrás de operações realizadas nos seus dados. As operações de uma classe são declaradas através dos seus métodos – instruções que operam nos dados de um objecto para obter um dado resultado. Os métodos acedem aos dados que estão escondidos de outros objectos. Métodos Os métodos são invocados como operações em objectos através das suas referências usando o operador ponto. referência.método ( listaDeParâmetros ) Cada parâmetro tem um tipo específico: tipo primitivo ou tipo referência. Os métodos determinam as mensagens que os objectos podem receber. Os métodos em Java só podem ser criados como parte de uma classe. Um método só pode ser chamado para um objecto (excepto os métodos estáticos). O acto de chamar um método para um objecto é normalmente referido como o envio de uma mensagem a um objecto. Os métodos também têm um tipo de retorno que é declarado antes do nome do método. A palavra-chave return causa o abandono do método e se existe uma expressão à frente da instrução return, o valor dessa expressão é o resultado da execução do método. Quando o tipo de retorno é void, a palavra-chave return (sem expressão à frente) só é usada para sair do método, ou pode não ser necessária se a execução atinge o fim do método. Consideremos a necessidade de criar objectos representativos de astros através do nome, número de identificação único, e astro em torno do qual orbita. A classe poderia ser definida do seguinte modo: public class Astro { private int numId; private String nome; private Astro orbitaEmVolta; private static int proxNumId = 0; public Astro() { numId = proxNumId ++; } DEI - ISEP Fernando Mouta 16 Programação Orientada por Objectos public Astro( String nomeAstro) { this(); nome = nomeAstro; } public Astro( String nomeAstro, Astro orbita) { this(nomeAstro); orbitaEmVolta = orbita; } public String toString() { String descr = numId + “(“ + nome + “)”; if ( orbitaEmVolta != null ) descr += “ orbita em volta de “ + orbitaEmVolta.toString(); return descr; } } Criação e inicialização de objectos que representam astros: Astro Astro Astro Astro a1 a2 a3 a4 = = = = new new new new Astro(“Sol”); Astro(“Terra”, a1); Astro(“Lua”, a2); Astro(“Marte”, a1); O método toString() é um método especial: se um objecto tem um método público designado toString que não recebe argumentos e retorna uma String, quando esse objecto é usado numa concatenação de strings, o método toString é implicitamente invocado para obter uma String. O resultado da execução do seguinte código: System.out.println(“Astro “ + a1); System.out.println(“Astro “ + a2); System.out.println(“Astro “ + a3); Será: Astro 0 (Sol) Astro 1 (Terra) orbita em volta de 0 (Sol) Astro 2 (Lua) orbita em volta de 1 (Terra) orbita em volta de 0 (Sol) Em todas as classes que construímos deveríamos definir o método toString(). No entanto se nenhum método toString() está definido numa classe, essa classe herda o método toString() da classe Object, o qual retorna uma string relativa ao tipo de objecto, mas pouco expressiva. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 17 Passagem de parâmetros na chamada de métodos Em muitas linguagens de programação há dois modos de passar argumentos na chamada de métodos: • por valor • por referência Quando um parâmetro é passado por valor, uma cópia do valor do argumento é usada pelo método chamado. Quando um parâmetro é passado por referência, o método chamado acede directamente esse parâmetro e através dele pode modificar os dados do parâmetro. A passagem por referência aumenta a eficiência porque elimina a sobrecarga de copiar grandes quantidades de dados mas enfraquece a segurança, porque o método chamado acede directamente os dados. Em Java todos os parâmetros são passados por valor: • Os tipos de dados primitivos são sempre passados por valor. • Os objectos são passados através dos valores das variáveis referência (que referenciam os respectivos objectos). Esses valores das variáveis referência são também passados por valor. Assim o método chamado usa uma cópia do valor da variável que referencia o objecto e acede directamente os dados do objecto podendo modificar esses dados. Por isso, algumas vezes se diz incorrectamente que os objectos são passados por referência, mas não é verdade, porque o método chamado não acede directamente à variável referência do objecto usada na invocação do método. Se o método chamado alterar o valor da variável referência do objecto (deixando de referenciar o objecto, passando a ter o valor “null” ou a referenciar outro objecto), no programa que chamou o método, a variável que referencia o objecto não sofre alteração. Para passar um objecto a um método como parâmetro: • na chamada do método especifica-se simplesmente a referência ao objecto • dentro do método o valor da variável parâmetro (variável referência) é uma cópia do valor especificado na invocação como argumento. Quando se retorna informação de um método através de uma instrução “return”, os tipos de dados primitivos são sempre retornados por valor e os objectos são retornados através da referência ao objecto. Um método pode retornar mais que um resultado por uma qualquer das seguintes formas: • Retornando uma referência a um objecto que armazena os resultados como campos de dados. • Possuindo um ou mais parâmetros que referenciam objectos nos quais se podem armazenar os resultados. • Retornando um array que contém os resultados. DEI - ISEP Fernando Mouta 18 Programação Orientada por Objectos Nomes dos argumentos dos métodos Quando se declara um argumento de um método especifica-se um nome para esse argumento. Esse nome é usado dentro do corpo do método. Um argumento de um método pode ter o mesmo nome que as variáveis membros da classe. Neste caso, o argumento esconde a variável membro da classe. Exemplo num construtor: class Circulo { int x, y, raio; public Circulo( int x, int y, int raio) { ... } } O uso de x, y e raio dentro do construtor refere-se aos argumentos e não aos campos de dados da classe. Para aceder aos campos de dados da classe deve-se referenciar através de this (o objecto corrente): class Circulo { int x, y, raio; public Circulo( int x, int y, int raio) { this.x = x; this.y = y; this.raio = raio; } } Esconder identificadores deste modo deliberadamente só é considerado uma boa prática de programação neste uso em construtores e em métodos de acesso aos campos de dados de um objecto. this é usado no sentido de este objecto, o objecto corrente. this só pode ser usada dentro de um construtor ou método e produz uma referência para o objecto corrente (o objecto construído ou o objecto para o qual o método foi chamado). Fernando Mouta DEI - ISEP Programação Orientada por Objectos 19 Esta referência (this) pode ser usada como qualquer outra referência de um objecto. No entanto se dentro de um método se chama outro método da mesma classe não é necessário usar this – basta chamar o método. A palavra-chave this só deve ser usada quando é necessário usar explicitamente a referência ao objecto corrente. A palavra-chave this é usada normalmente nos seguintes casos: • Dentro de um método não estático ou dentro de um construtor para referenciar o objecto corrente no qual o método foi invocado se algum dos argumentos do método tem o mesmo nome que um campo do objecto. • Para passar uma referência do objecto corrente como um parâmetro a outros métodos. • Em instruções return para retornar a referência ao objecto corrente. No exemplo apresentado a seguir this é usado numa instrução return para retornar a referência ao objecto corrente: // Folha.java public class Folha { private int i = 0; Folha incremento() { i++; return this; } void mostrar() { System.out.println(“i = “ + i); } public static void main(String args []) { Folha f = new Folha(); f.incremento().incremento().incremento().mostrar(); } } Métodos estáticos São métodos que não estão associado com qualquer objecto particular de uma classe. Um uso importante dos métodos estáticos é permitir chamar esses métodos sem criar qualquer objecto. Um exemplo é o método main() – ponto de entrada para correr uma aplicação. Os métodos estáticos não podem aceder directamente membros não estáticos (campos de dados ou métodos) – chamando simplesmente esses outros membros sem referir um nome de um objecto - dado que os membros não estáticos devem estar ligados a objectos particulares. class Exemplo { int x; public static void setX( int xx) [ x = xx; } } DEI - ISEP Fernando Mouta 20 Programação Orientada por Objectos O compilador daria o seguinte erro: não pode fazer uma referência estática a uma variável não estática. Para corrigir, poderíamos alterar a definição do método setX passando-o para método de instância ou alterar o campo de dados x colocando-o estático. Vamos corrigir a definição da classe através da alteração do campo de dados x: class Exemplo { static int x; public static void setX( int xx) [ x = xx; } } Consideremos o seguinte código: Exemplo ex1 = new Exemplo(); Exemplo ex2 = new Exemplo(); ex1.x = 1; ex2.x = 2; Exemplo.setX(3); System.out.println(“ex1.x = “ + ex1.x); System.out.println(“ex2.x = “ + ex2.x); Produz: ex1.x = 3; ex2.x = 3; Um método estático, como qualquer outro método, pode criar ou usar objectos com nome, incluindo objectos do seu tipo. Há 2 maneiras de referenciar um método estático: através de um objecto como para qualquer outro método ou através do nome da classe (preferível). Um método estático é invocado em nome de toda a classe e não em nome de um objecto específico da classe, e por isso se designa por método da classe. Num método estático não se pode usar a plavra-chave this. Overloading de Métodos Um nome de um método é um nome para uma acção ou conjunto de acções. Um nome apropriado torna o programa mais fácil de ser comprendido e mantido. Na linguagem natural uma mesma palavra pode ter vários significados, que são deduzidos pelo contexto. Não é necessário ter identificadores únicos para cada conceito. Em muitas linguagens de programação, como em C, é necessário ter um identificador único para cada função. Em Java podem existir métodos ou construtores com o mesmo nome e diferente lista de tipos de parâmetros. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 21 Cada método tem uma assinatura. Assinatura de um método – é o seu nome em conjunto com a lista dos tipos dos seus parâmetros. “Overloading” – é a existência de mais que um método com o mesmo nome mas diferente lista de tipos de parâmetros e portanto diferentes assinaturas, na mesma classe ou numa hierarquia de superclasse e subclasses. Designa-se por “overloading” porque o nome do método tem um significado sobrecarregado (“overloaded”), isto é, tem mais que um significado. A assinatura não inclui o tipo de retorno ou a lista de “thrown exceptions”, e portanto não se pode usar métodos “overload” baseado nesses factores. É um erro de sintaxe se um método numa classe e outro método nessa classe ou numa sua subclasse ou superclasse têm a mesma assinatura mas diferentes tipos de retorno. Ex.: public boolean orbita(Astro corpo) { return (orbitaEmVolta == corpo); } public boolean orbita(int id) { return (orbitaEmVolta != null && orbitaEmVolta.numId == id); } Uso de Métodos para Controlar o Acesso No exemplo da classe Astro o campo numId é colocado (recebe um valor) de um modo automático e correcto pelo construtor quando se cria um objecto. No entanto este campo numId, declarado com public, permite o acesso de fora da classe. Este acesso é útil para leitura do valor mas não deveria ser possível alterar o valor. Para tornar o campo só de leitura (“read only”) fora da classe deve-se esconder o campo declarando-o private e fornecer um novo método público para permitir a leitura do campo fora da classe. Ex.: public class Astro { private int numId; private String nome; private Astro orbita; private static int proxNumId = 0; Astro() { numId = proxNumId ++; DEI - ISEP Fernando Mouta 22 Programação Orientada por Objectos } public int getNumId() { return numId; } } Deste modo o campo numId só pode ser modificado por métodos dentro da classe Astro. Métodos que regulam o acesso a dados internos designam-se por métodos de acesso. Normalmente os campos de dados de uma classe declaram-se como private e adicionam-se métodos para colocar (set) e retribuir (get) os valores desses campos de dados. Os métodos para colocar valores nos campos de dados devem verificar a consistência dos dados só permitindo alterações se adequadas. A representação interna dos dados (usada dentro da classe) não interessa aos clientes da classe e por isso deve estar escondida deles, o que facilita a possibilidade de modificação da implementação da classe e também simplifica a percepção que os clientes têm da classe. Os métodos “public” apresentam aos clientes da classe os serviços que a classe fornece. Na programação orientada por objectos as variáveis instância e os métodos estão encapsulados dentro de um objecto e por isso os métodos podem aceder às variáveis instância. Os acesso de fora da classe a dados “private” devem ser cuidadosamente controlados por métodos da classe. Por exemplo um método que permita modificar dados “private” deve validar os valores dos dados e traduzir a representação dos dados para a forma usada na implementação. Poderá parecer equivalente entre disponibilizar métodos “public” para ler e alterar valores de variáveis instância ou declarar as variáveis instância “public”. A diferença reside no facto de que, se as variáveis instância são “public”, podem ser lidas e alteradas por qualquer método do programa, enquanto que se forem “private” e se se incluir métodos “public” para ler e alterar, essas variáveis podem ser lidas por qualquer método do programa (mesmo de outras classes), mas o método que efectua a sua alteração (método da classe) deve controlar o valor e o formato dos dados. Ainda um método para alterar o valor de uma variável instância pode retornar valores indicando que foi feita uma tentativa de atribuição de valores inválidos a um objecto da classe. Isto permite aos clientes da classe testar os valores retornados por estes métodos para determinar se os objectos que estão a manipular são válidos e tomar acções apropriadas se eles não são válidos. Libertação da memória ocupada por objectos Fernando Mouta DEI - ISEP Programação Orientada por Objectos 23 A memória ocupada por um objecto criado com new, pode ser libertada quando já não se precisa do objecto, deixando de o referenciar (terminando o âmbito de validade (“scope “) da variável que o referencia, ou alterando o valor dessa variável). Variáveis declaradas dentro de um bloco ({ ... }) perdem o âmbito de validade quando a execução do programa sai do bloco. Ex.: { String s = “Uma string”; System.out.println(s); } Em Java, a memória ocupada por objectos não referenciados é automaticamente libertada pelo “garbage collector”, que corre em background, e verifica para todos os objectos criados com new os que já não estão referenciados. Um objecto não é atingível, quando não existe nenhuma referência ao objecto em qualquer variável de qualquer método em execução, ou em qualquer campo ou elemento de array de uma dessas variáveis. O garbage collector elimina a necessidade de libertar explicitamente a memória ocupada pelos objectos, mas só realiza essa libertação de memória quando é necessário evitar que a memória fique cheia ou porque necessita de mais espaço de memória. Inicialização de variáveis por omissão Variáveis membros de classes são automaticamente inicializadas quando se criam objectos. Para cada objecto é alocado memória no heap e preenchida com 0´s do que resultam os seguintes valores conforme os tipos dos campos de dados: Tipos: Valores por omissão: byte, short, int, long float double char boolean referência de objectos 0 0.0f 0.0 ‘\u0000’ false null Variáveis locais – não campos de dados de uma classe, mas declaradas em métodos – não são automaticamente inicializadas. Estas variáveis são armazenadas na stack e é necessário inicializá-las antes de as usar senão dá erro de compilação. Arrays - Para tipos primitivos de dados: int a [] = new int [2]; a[0] = 7; a[1] = 10; DEI - ISEP Fernando Mouta 24 Programação Orientada por Objectos Criação por inicialização directa: int a[] = {7, 10}; Arrays de objectos Considerando a seguinte definição da classe Ponto: class Ponto { double x, y; public Ponto(int x, int y) { this.x = x; this.y = y; System.out.println("Ponto (" + x + ", " + y + ")"); } } Das instruções seguintes, a 1.ª e a 3.ª criam objectos, enquanto que a 2.ª não cria qualquer objecto, como se poderia verificar pela execução do programa através das mensagens emitidas pelo construtor. Ponto p1 = new Ponto(2, 3); Ponto p2 = p1; new Ponto(4, 5); Quando se cria um array de objectos o que realmente fica criado é um array de referências inicializadas a “null”. Depois é necessário atribuir objectos às referências. Ex. Ponto [] pontos = new Ponto[3]; pontos[0] = new Ponto(0, 0); pontos[1] = new Ponto(1, 1); pontos[2] = new Ponto(2, 2); ou, de outro modo, se se pretendesse ler os valores do teclado: Ponto [] pontos = new Ponto[3]; int x,y; for (int i=0; i < pontos.length ; i++) { x = lerNumero(“Coordenada x do ponto “ + (i+1)); y = lerNumero(“Coordenada y do ponto “ + (i+1)); pontos[i] = new Ponto(x, y); } Criação de um array de objectos por inicialização directa: Ponto [] pontos = { new Ponto(0, 0), new Ponto(1, 1), new Ponto(2, 2) }; Em Java 1.1: Fernando Mouta DEI - ISEP Programação Orientada por Objectos 25 Ponto [] pontos = new Ponto [] { new Ponto(0, 0), new Ponto(1, 1), new Ponto(2, 2) } Uma vírgula final na lista de inicializadores é opcional (para fácil manutenção): Ponto [] pontos = { new Ponto(0, 0), new Ponto(1, 1), new Ponto(2, 2), } Array de arrays: Ponto [][] paresDePontos = { { new Ponto(0, 0), new Ponto(0,1) }, { new Ponto(1, 1), new Ponto(1,2) }, { new Ponto(2, 2), new Ponto(2,3) }, } Reutilização de Código Uma das características mais importantes do Java é a possibilidade de reutilização de código. A aproximação tradicional de reutilização de código consiste em copiar e adaptar. Em Java a reutilização de código consiste na criação de classes que usam outras classes já existentes. Para isso uma classe deve ser projectada para resolver problemas genéricos e não casos específicos. Há 2 maneiras de reutilizar código: 1. Dentro de uma nova classe criam-se objectos de classes já existentes. Este método designa-se por composição porque a nova classe é composta de objectos de classes existentes. 2. Criando uma nova classe como um tipo de uma classe existente. Usa-se a forma da classe existente e adiciona-se código. Este método designa-se por herança. São dois modos de criar novos tipos a partir de tipos existentes. Mas enquanto na composição apenas se utiliza a funcionalidade do código e não a sua forma, na herança utiliza-se a forma do código. Criação de Subclasses Declara-se que uma classe é um asubclasse de outra na declaração da classe: class SubClasse extends SuperClasse { ... } DEI - ISEP Fernando Mouta 26 Programação Orientada por Objectos Uma subclasse herda variáveis e métodos da superclasse e de todas as superclasses desta. Ex.: import java.awt.Color; class Ponto { double x, y; public Ponto(int x, int y) { this.x = x; this.y = y; System.out.println("Ponto (" + x + ", " + y + ")"); } } class Pixel extends Ponto { Color cor; public Pixel() { super(0,0); cor = null; } } Herança Herança é uma forma de “reusar software” (“software reusability”) na qual novas classes são criadas a partir de classes existentes absorvendo (herdando) os seus atributos e comportamentos e acrescentando novos atributos e comportamentos que as novas classes necessitem. Assim, ao criar uma nova classe, em vez de incorporar todas as variáveis instância e métodos instância novos que a classe necessita, o programador pode designar (especificar) que a nova classe herda variáveis instância e métodos instância de uma outra classe previamente definida. A nova classe designa-se por subclasse e a classe da qual herda variáveis e métodos instância designa-se por superclasse. Todo o objecto da subclasse é também um objecto da superclasse, mas o inverso não é verdade. Como uma subclasse normalmente adiciona variáveis instância e métodos instância próprios, uma subclasse é geralmente maior que a sua superclasse (tem mais características: atributos e comportamentos) mas também é mais específica e representa um menor grupo de objectos. “Overriding” de Métodos (Reescrita de Métodos) Fernando Mouta DEI - ISEP Programação Orientada por Objectos 27 Uma subclasse começa do mesmo que a superclase mas tem a possibilidade de definir adições ou substituições das características herdadas da superclasse. Pode acontecer que uma subclasse possa herdar variáveis instância ou métodos instância que não necessita ou que expressamente não deva ter. Quando um membro de uma superclasse não é apropriado para uma subclasse, esse membro pode ser “overriden” (sobreposto, reescrito) na subclasse através da definição de uma variável instância com o mesmo nome e tipo ou de um método com a mesma assinatura. Quando um método “overrides” (se sobrepõe) a um método da superclasse, o método da superclasse pode ser acedido da subclasse precedendo o nome do método da superclasse com “super” seguido de ponto. Quando o método é invocado da subclasse sem a referência “super.” a versão da subclasse é automaticamente seleccionada. Ex.: import java.awt.Color; class Ponto { double x, y; public void limpar() { x = 0; y = 0; } } class Pixel extends Ponto { Color cor; public void limpar() { super.limpar(); cor = null; } } Construtores em Classes Estendidas Quando se estende uma classe a nova classe deve escolher um dos construtores da superclasse para invocar. Um objecto terá uma parte controlada pela superclasse que deve ser iniciada e outra parte controlada pela subclasse relativa aos campos de dados adicionados. Num construtor de uma subclasse, pode-se invocar directamente um construtor da superclasse usando a instrução super(), que é uma invocação explícita de um construtor da superclasse. Se não se invoca explicitamente um construtor da superclasse como primeira instrução executável de um construtor, o construtor sem argumentos da superclasse é automaticamente invocado antes de qualquer instrução ser executada. DEI - ISEP Fernando Mouta 28 Programação Orientada por Objectos Se a superclasse não tem um construtor sem argumentos deve-se invocar explicitamente um dos construtores da superclasse ou então outro construtor da mesma classe usando this(). Se uma classe estendida não declara qualquer construtor, o compilador cria um construtor por omissão (sem argumentos) que começa por invocar o cosntrutor sem argumentos da superclasse: public class SubClasse extends SuperClasse { public SubClasse () { super(); } } Conversão implícita e explicita entre referências de objectos Java efectua a conversão implícita de uma referência a um objecto de uma classe para uma referência a um objecto de uma superclasse da classe. Esta conversão designa-se por “upcasting” (“casting up” na hierarquia de classes) ou conversão segura (“safe casting”) porque é sempre válida. Ex.: Como todas as classes são subclasses da classe Object, Object é um tipo referência genérico que pode referenciar qualquer objecto de qualquer classe: Object o = new Ponto(); o = “Objecto String”; A conversão de uma referência a um objecto de uma classe para uma referência a uma subclasse só é possível se o objecto é realmente um objecto da subclasse e é necessário efectuar uma conversão explícita. Ex.: Object o1 = new Ponto(); Ponto o2 = (Ponto) o1; Esta conversão designa-se por “downcasting” (“casting down” na hierarquia de classes) ou conversão insegura (“unsafe casting”) porque nem sempre é válida. No entanto esta conversão é necessária para aceder às funcionalidades que a subclasse adiciona. Uma conversão explícita para uma classe que não é subclasse da classe a que o objecto pertence dá erro de compilação. Uma conversão explícita para uma subclasse é potencialmente correcta, mas se em tempo de execução não é válida, porque o objecto não é realmente um objecto da subclasse, uma excepção “ClassCastException” é lançada. Embora um objecto de uma subclasse possa ser tratado como um objecto da superclasse os tipos subclasse e superclasse são diferentes. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 29 No entanto, é possível referenciar um objecto de uma subclasse com uma referência da superclasse ( Java efectua uma conversão implícita ). Através da referência da superclasse só se pode aceder a membros da superclasse. Referência a membros só da subclasse (membros não herdados da superclasse), através da referência da superclasse causa erro de sintaxe. Referenciar um objecto de uma superclasse com uma referência de uma subclasse causa erro de sintaxe a não ser que o objecto seja realmente um objecto da subclasse e após um "cast" do tipo superclasse para o tipo subclasse. Se o objecto referenciado não é um objecto da subclasse, não tem valores para as variáveis instância que pertencem só à subclasse, pelo que não faz sentido referenciá-lo através de uma variável referência da subclasse. Polimorfismo O mecanismo de polimorfismo do Java permite que usando uma referência de uma superclasse para referenciar um objecto, instância de uma subclasse, e invocando um método que exista na superclasse e que também exista (“overriden”) em uma subclasse, o programa escolherá dinamicamente (isto é, em tempo de execução) o método correcto da subclasse. Isto designa-se por ligação dinâmica de método ("dynamic method binding"). Através do uso de polimorfismo, uma invocação de um método pode causar diferentes acções dependendo do tipo de objecto que recebe a chamada. Ex.: class FormaGeom { void desenhar() { System.out.println(“FormaGeom.desenhar()”) } } class Circunferencia extends FormaGeom { void desenhar() { System.out.println(“Circunferencia.desenhar()”) } } class Quadrado extends FormaGeom { void desenhar() { System.out.println(“Quadrado.desenhar()”) } } class Triangulo extends FormaGeom { void desenhar() { System.out.println(“Triangulo.desenhar()”) } } DEI - ISEP Fernando Mouta 30 Programação Orientada por Objectos public class Teste { public static FormaGeom formaGeomAleat() { switch ( (int) (Math.random() * 3) ) { case 0 : return new Circunferencia(); case 1 : return new Quadrado(); case 2 : return new Triangulo(); } } public static void main( String [] args ) { FormaGeom [] fg = new FormaGeom[6]; for (int i= 0; i < fg.length; i++) fg[i] = formaGeomAleat(); for (int i= 0; i < fg.length; i++) fg[i].desenhar(); } } Exemplo de saída produzida pelo programa: Circunferencia.desenhar() Triangulo.desenhar() Circunferencia.desenhar() Quadrado.desenhar() Triangulo.desenhar() Circunferencia.desenhar() Programas que invocam comportamento polimórfico podem ser escritos independentemente dos tipos de objectos para os quais as mensagens (isto é, as chamadas de métodos) são enviadas. Podem-se adicionar novos tipos de objectos que respondem a mensagens existentes sem modificar o sistema, o que promove a facilidade de extensão (extensibilidade). Como uma classe implementa um tipo, normalmente serão instanciados (criados) objectos dessa classe. Contudo há casos nos quais é útil definir classes para as quais não se pretende instanciar objectos mas que se destinam apenas a ser usadas como superclasses, para permitir usar os mecanismos de herança e polimorfismo. Estas classes designam-se por abstractas e não podem ser instanciados objectos destas classes. Uma classe abstracta tem de ser declarada com a palavra-chave "abstract". Uma classe que contenha um método abstracto (declarado com a palavra chave "abstract") é uma classe abstracta e por isso deve ser explicitamente declarada como uma classe abstracta. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 31 Se uma subclasse deriva de uma superclasse com um método abstracto, e se na subclasse não se fornece nenhuma definição para esse método abstracto (esse método não é "overriden" na subclasse) então esse método permanece abstracto na subclasse, pelo que a subclasse é também uma classe abstracta e assim deve ser explicitamente declarada. A declaração de um método como abstracto numa classe obriga que esse método seja “overriden” (reescrito) nas subclasses para que se possam instanciar objectos dessas subclasses. Embora não se possam instanciar objectos de superclasses abstractas, pode-se declarar referências de superclasses abstractas, as quais podem ser usadas para permitir manipulações polimórficas de objectos de subclasses. Polimorfismo é particularmente adequado para implementar sistemas de software por camadas ("layered"). Ex.: abstract class FormaGeom { public abstract void desenhar(); public String toString() { return “Forma Geometrica”; } } class Ponto { protected double x,y; public Ponto( double x, double y ) {this.x = x; this.y = y; } public String toString() { return “Ponto de coordenadas = (“ + x + “, “ + y + “)”; } } class SegmRecta extends FormaGeom { protected Ponto p1, p2; public SegmRecta( double x1, double y1, double x2, double y2 ) { p1 = new Ponto(x1, y1); p2 = new Ponto(x2, y2); } public double comprimento() { double dx = p1.x - p2.x; double dy = p1.y - p2.y; return Math.sqrt(dx*dx + dy*dy); } public void desenhar() { System.out.println("\tDesenhar um segmento de recta"); } public String toString() { return super.toString() + ": Segmento de Recta de comprimento = " + comprimento(); } DEI - ISEP Fernando Mouta 32 Programação Orientada por Objectos } class Circunferencia extends FormaGeom { protected double raio; public Circunferencia( double r ) { raio = r ; } public void desenhar() { System.out.println("\tDesenhar uma circunferencia"); } public String toString() { return super.toString() + “: Circunferencia de raio = “ + raio; } } class Quadrado extends FormaGeom { protected double lado; public Quadrado( double l ) { lado = l; } public void desenhar() { System.out.println("\tDesenhar um quadrado"); } public String toString() { return super.toString() + “: Quadrado de lado = “ + lado; } } class Triangulo extends FormaGeom { protected double base, altura; public Triangulo( double b, double a ) { base = b; altura = a; } public void desenhar() { System.out.println("\tDesenhar uma triangulo"); } public String toString() { return super.toString() + “: Triangulo de base = “ + base + “ e de altura = “ + altura; } } public class Teste { public static void main(String args []) throws java.io.IOException { FormaGeom fg []= new FormaGeom [3]; fg[0] = new SegmRecta(4, 5, 8, 9); fg[1] = new Circunferencia(4); fg[2] = new Quadrado(3); fg[3] = new Triangulo(4, 3); for (int i= 0; i<fg.length; i++) { System.out.println( fg[i]; fg[i].desenhar(); } System.in.read(); } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 33 } Saída produzida pelo programa: Forma Geometrica: Segmento de Recta de comprimento = 5.65685 Desenhar um segmento de recta Forma Geometrica: Circunferencia de raio = 4 Desenhar uma circunferencia Forma Geometrica: Quadrado de lado = 3 Desenhar um quadrado Forma Geometrica: Triangulo de base = 4 e de altura = 3 Desenhar uma triangulo Pretendemos que a classe FormaGeom contenha todas as características que as formas geométricas têm em comum: campos de dados e métodos. Mas a classe genérica FormaGeom não deve implementar o método desenhar, porque não representa uma forma concreta. Este método deve ser declarado abstracto. Os métodos abstractos são definidos sem a implementação; não têm corpo, só têm assinatura terminada por ;. Classes abstractas Qualquer classe com pelo menos 1 método abstracto é uma classe abstracta e tem de ser declarada como tal. Uma classe abstracta não pode ser instanciada. Uma classe abstracta refere-se a conceitos tão genéricos que não é útil criar objectos dese tipo. Uma classe também pode ser declarada abstracta mesmo sem métodos abstractos, ficando inibida a possibilidade de ser instanciada. Uma subclasse de uma classe abstracta pode ser instanciada se reescreve todos os métodos abstractos da superclasse e fornece uma implementação para eles. Se uma subclasse de uma classe abstracta não implementa pelo menos 1 método abstracto que herde, a subclasse é abstracta e como tal deve ser declarada. Interfaces Interfaces, como classes e métodos abstractos, fornecem declarações de comportamentos que outras classes devem implementar. A herança simples é restritiva quando pretendemos usar um determinado comportamento em diferentes ramos de uma árvore hierárquica. Com herança múltipla, uma classe pode herdar de mais que uma superclasse obtendo ao mesmo tempo atributos e comportamentos de todas as superclasses. No entanto a múltipla herança torna a linguagem de programação muito mais complicada. Java só possui herança simples. Para resolver o problema da necessidade de comportamentos comuns em classes em diferentes ramos de uma árvore hierárquica, Java tem outra hierarquia de comportamentos de classes, a hierarquia de interfaces. DEI - ISEP Fernando Mouta 34 Programação Orientada por Objectos Assim quando criámos uma classe, essa classe só tem uma superclasse, mas pode escolher diferentes comportamentos da hierarquia de interfaces. Um interface em Java é uma colecção de comportamentos abstractos que podem ser incluídos em qualquer classe para adicionar a essa classe comportamentos que não são fornecidos pelas superclasses. Um interface em Java só contém definições de métodos abstractos e constantes estáticas – não contém variáveis instância nem implementação de métodos. Os interfaces são como as classes, declarados em ficheiros fonte, compilados em ficheiros .class e podem ser usados para tipos de dados de variáveis. São diferentes das classes porque não podem ser instanciados. Ex.: public interface Imposto { double calculoDoImposto(); } Ao contrário das classes um interface pode ser adicionado a uma das classes que já é uma subclasse de outra classe. Pode-se pretender usar este interface em muitas classes diferentes: roupa, comida, carros, etc. Seria inconveniente que todos estes objectos derivassem de uma única classe. Para além disso cada tipo diferente poderá ter um modo diferente de cálculo de imposto. Assim define-se o interface Imposto e cada classe deve implementar esse interface. Um interface pode conter vários métodos (incluindo métodos “overloaded”) e campos de dados, mas estes têm de ser static final. Ex.: abstract class FormaGeom { public abstract void desenhar(); public String toString() { return "Forma Geometrica"; }; } interface Dimensoes { double area(); double perimetro(); } class Ponto { protected double x,y; public Ponto( double x, double y ) {this.x = x; this.y = y; } public String toString() { return super.toString() + ": Ponto de Coordenadas = (" + x + ", " + y + ")"; } } class SegmRecta extends FormaGeom { protected Ponto p1, p2; public SegmRecta( double x1, double y1, double x2, double y2 ) { p1 = new Ponto(x1, y1); p2 = new Ponto(x2, y2); } public double comprimento() { double dx = p1.x - p2.x; double dy = p1.y - p2.y; Fernando Mouta DEI - ISEP Programação Orientada por Objectos 35 return Math.sqrt(dx*dx + dy*dy); } public void desenhar() { System.out.println("\tDesenhar um segmento de recta"); } public String toString() { return super.toString() + ": Segmento de Recta de comprimento = " + comprimento(); } } class Circunferencia extends FormaGeom implements Dimensoes { protected double raio; public Circunferencia( double r ) { raio = r; } public double area() { return Math.PI * raio * raio; } public double perimetro() { return 2 * Math.PI * raio; } public void desenhar() { System.out.println("\tDesenhar uma circunferencia"); } public String toString() { return super.toString() + ": Circunferencia de raio = " + raio; } } class Quadrado extends FormaGeom implements Dimensoes { protected double lado; public Quadrado( double l ) {lado = l; } public double area() { return lado * lado; } public double perimetro() { return 4 * lado; } public void desenhar() { System.out.println("\tDesenhar um quadrado"); } public String toString() { return super.toString() + ": Quadrado de lado = " + lado; } } class Triangulo extends FormaGeom implements Dimensoes { protected double base, altura; public Triangulo( double b, double a ) { base = b; altura = a; } public double area() { return base * altura / 2; } public double perimetro() { return base+2*Math.sqrt(base*base/4+altura*altura); } public void desenhar() { System.out.println("\tDesenhar uma triangulo"); } public String toString(){ return super.toString() + ": Triangulo de base = "+base+" e de altura = "+altura; } DEI - ISEP Fernando Mouta 36 Programação Orientada por Objectos } public class Teste { public static void main(String args []) { FormaGeom fg []= new FormaGeom [4]; fg[0] = new SegmRecta(4, 5, 8, 9); fg[1] = new Circunferencia(4); fg[2] = new Quadrado(3); fg[3] = new Triangulo(4, 3); for(int i= 0; i<fg.length; i++) { System.out.println( fg[i] ); fg[i].desenhar(); } Dimensoes d []= new Dimensoes [3]; d[0] = (Circunferencia) fg[1]; d[1] = (Quadrado) fg [2]; d[2] = (Triangulo) fg [3]; for(int i= 0; i<d.length; i++) { System.out.println( d[i] + “\n\t Area = “ + d[i].area() + “\n\t Perimetro = “ + d[i].perimetro() ); } } Saída produzida pelo programa: Forma Geometrica: Segmento de Recta de comprimento = 5.65685 Desenhar um segmento de recta Forma Geometrica: Circunferencia de raio = 4 Desenhar uma circunferencia Forma Geometrica: Quadrado de lado = 3 Desenhar um quadrado Forma Geometrica: Triangulo de base = 4 e de altura = 3 Desenhar uma triangulo Forma Geometrica: Circunferencia de raio = 4 Area = 50.2655 Perimetro = 25.1327 Forma Geometrica: Quadrado de lado = 3 Area = 9 Perimetro = 12 Forma Geometrica: Triangulo de base = 4 e de altura = 3 Area = 6 Perimetro = 11.2111 Fernando Mouta DEI - ISEP Programação Orientada por Objectos 37 Outro exemplo do uso de interfaces: Faça um programa que construa uma tabela para os quadrados dos inteiros de 1 a 5 e outra para os cubos dos inteiros dos múltiplos de 10 entre 10 e 50. interface Funcao { public abstract int f(int x); } class Quadrado implements Funcao{ public int f(int x) { return x*x; } } class Cubo implements Funcao { public int f(int x) { return x*x*x; } } class Tabela { public static void tabela(Funcao t, int a, int b, int incr) { System.out.println( "x\t| f(x)\n--------------"); for (int x=a; x<b; x+=incr) System.out.println(x+"\t| "+t.f(x)); System.out.println(); } public static void main(String args []){ Funcao t = new Quadrado(); tabela(t, 1, 5, 1); t = new Cubo(); tabela(t, 10, 50, 10); } Saída produzida pelo programa: x | f(x) -------------1 |1 2 |4 3 |9 4 | 16 5 | 25 x | f(x) -------------10 | 1000 20 | 8000 30 | 27000 40 | 64000 DEI - ISEP Fernando Mouta 38 Programação Orientada por Objectos 50 | 125000 Polimorfismo A herança é usada com 2 objectivos: • reutilização de código, e • implementação de polimorfismo. O benefício do polimorfismo é permitir que numa hierarquia de tipos com o mesmo interface (os mesmos métodos públicos), código que funciona com um tipo genérico também funcione com qualquer objecto que seja subtipo desse tipo. Isto simplifica a escrita de código, a leitura e compreensão e a manutenção. Os comportamentos comuns definem-se na classe base (superclasse). Qualquer objecto de um subtipo pode receber as mesmas mensagens que a classe base, e pode ser tratado como um objecto da classe base. Ainda permite uma fácil extensão de um programa com um mínimo de modificações, pois código que funciona com um tipo genérico também funciona com novos subtipos que se criem. Composição vs Herança Usa-se a composição quando se pretende as características de uma classe existente dentro da nova classe, mas não o interface da classe existente. Usa-se a herança quando se pretende especializar uma classe existente para um fim particular. Um modo claro de determinar se se deve usar composição ou herança obtém-se determinando a relação entre os objectos da classe que se pretende criar e da classe existente: A relação tem-um (“has-a”) é expressa através da composição e a relação é-um (“is-a”) exprime-se através da herança. Ex. de composição: Ex. de herança: Uma circunferência tem um ponto que é o centro mas uma circunferência não é um ponto. Um carro é um veículo mas um carro não contém um veículo. Packages Os packages permitem criar um grupo de classes relacionadas (logicamente). Usam-se para: • tornar mais fácil encontrar e usar classes, • evitar conflitos de nomes entre classes, • controlar o acesso de umas classes às outras. Um package é uma unidade de uma biblioteca (“library”) de classes formada por um dado conjunto de classes. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 39 Conflitos entre nomes Os nomes de todos os membros de uma classe estão isolados dos nomes dos membros de outras classes. Mas os nomes de classes podem coincidir. Para criar um package coloca-se uma classe dentro dele. Para incluir uma classe num package coloca-se a instrução package no início do ficheiro fonte no qual a classe está definida. package utilitarios; O nome do package é implicitamente prefixado a cada nome de classe (tipo) contido no package. Uma instrução package deve ser a primeira instrução de um ficheiro (para além dos comentários). O âmbito de validade de um instrução package estende-se a todo o ficheiro fonte. Em cada ficheiro só pode existir uma declaração de um package. Se não se usa a instrução package a classe pertence ao “unnamed package”. Todos os tipos que não especificam nome de package são tratados como pertencendo ao mesmo package, único para a máquina virtual em execução. Ficheiros fonte e ficheiros classe Um ficheiro fonte constitui uma unidade de compilação, pode conter mais que uma classe, mas só pode haver uma classe pública, que deve ter o mesmo nome que o ficheiro fonte (incluindo letras maiúsculas, mas excluindo a extensão .java). Depois de compilado obtém-se um ficheiro para cada classe, com o nome da classe e a extensão .class. Se um ficheiro fonte não contém nenhuma classe pública, pode ter qualquer nome. Os ficheiros classe devem ser colocados num directório cujo nome reflecte o nome do package, para que o compilador e o interpretador possam encontrar as classes. A variável de ambiente CLASSPATH (colocada através do sistema operativo ou do ambiente de desenvolvimento) contém um ou mais directórios que são usados como raízes para pesquisa de ficheiros .class. No ambiente de desenvolvimento Visual J++ a variável CLASSPATH é colocada seguindo os menus Tools, Options, Directories. Ex.: CLASSPATH=.;G:\JAVA\biblio Classes pertencentes ao: devem ser colocadas no directório package utilitarios; G:\JAVA\biblio\utilitarios Classes pertencentes ao: devem ser colocadas no directório package curso.graficos.trab2; G:\JAVA\biblio\curso\graficos\trab2 Para nomes de packages usam-se normalmente sempre letras minúsculas. DEI - ISEP Fernando Mouta 40 Programação Orientada por Objectos Acesso a packages Ao escrever código fora do package que necessite de classes (tipos) declarados dentro de um package pode-se: • preceder cada nome de um tipo pelo do package, ou • importar parte ou todo o package. Ex.: utilitarios.Classe1 c1 = new utilitarios.Classe1(); ou: import utilitarios.Classe1; import utilitarios.*; Classe1 c1 = new Classe1(); Código de um package importa outro código do package que necessite implicitamente. A instrução import deve ser colocada depois de uma declaração de package, mas antes de outro código. Colisão entre nomes de classes Se duas bibliotecas são importadas e incluem classes com o mesmo nome, mas não é efectuado nenhum acesso a qualquer uma dessas classes com o mesmo nome, não há problema. Se é necessário aceder a algumas dessas classes temos de especificar o nome completo da classe prefixando o nome do package. Compilação automática A primeira vez que se cria um objecto ou se acede a um membro estático de uma classe importada (seja por ex. classe A), o compilador procura o ficheiro com o nome da classe e extensão .class (A.class) no directório apropriado. Se também encontra um ficheiro com o nome da classe e extensão .java (A.java) e se este ficheiro é mais recente que o .class, automaticamente o recompila para gerar um ficheiro .class actualizado. Acesso a classes e membros de classes Java possui especificadores de acesso para permitir ao criador de uma biblioteca de classes especificar o que está disponível para o programador cliente e o que não está. Deste modo o criador da biblioteca pode fazer modificações (melhoramentos) sem afectar o código do programador cliente. Normalmente os campos de dados e alguns métodos não se destinam a ser usados directamente pelo programador cliente. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 41 Os níveis de acesso são: • public • friendly ou package (sem palavra-chave) • protected • private Acesso a classes Alguns especificadores de acesso podem ser usados para determinar que classes de uma biblioteca estão disponíveis para um utilizador dessa biblioteca. Uma classe pode ter 2 tipos de acesso: público e package ou friendly. public: declaradas com a palavra-chave “public”, tornam a classe acessível ao programador cliente a partir de qualquer package. friendly: não se coloca nenhum especificador de acesso, a classe só é acessível de outra qualquer do mesmo package, mas não de fora do package. No entanto os membros estáticos públicos de uma classe friendly são acessíveis de outros packages.Este tipo de acesso usa-se para classes que apenas suportam tarefas realizadas por outras classes.. Deste modo o criador da classe não necessita de criar documentação e mantém a possibilidade de poder alterá-la porque nenhum programa cliente depende da implementação particular da classe. Acesso a membros de classes Os especificadores de acesso public, protected e private colocados à frente na definição de cada membro (campo ou método) de uma classe controlam o acesso a esses membros. Acesso public Um membro de uma classe declarado com o modificador de acesso public pode ser acedido por qualquer programa que tenha acesso à classe desse membro. Acesso friendly É o tipo de acesso por omissão, quando não se especifica nenhum modificador de acesso. Todas as classes no mesmo package têm acesso a membros friendly, mas classes de outros packages não têm acesso. Acesso private Membros declarados com o modificador de acesso private só podem ser acedidos por métodos dentro da própria classe.. Normalmente campos de dados e métodos que apenas auxiliam outros métodos são declarados private para assegurar que não são usados acidentalmente e assim podem ser modificados sem que afectem outras classes no mesmo package. DEI - ISEP Fernando Mouta 42 Programação Orientada por Objectos Acesso protected Um membro declarado com o modificador de acesso protected pode ser acedido por todas as classes no mesmo package e por classes, mesmo que noutros packages, que herdem da classe a que o membro pertence. Uma classe que herde de outra classe (superclasse) herda todos os campos de dados e métodos dessa superclasse. Os membros privados e friendly da superclasse embora herdados não são acessíveis da subclasse, enquanto que os membros públicos são acessíveis não só da subclasse como também de todas as outras classes. O modificador de acesso protected é útil para permitir acesso às subclasses, sem permitir aceso a todas as outras classes não relacionadas. No entanto se uma classe B é uma subclasse de A pertencente a um package diferente, a classe B pode aceder a membros protected (campos de dados ou métodos) de A mas só em objectos do tipo B ou de subclasses de B. A classe B não pode aceder a membros protected de A em objectos do tipo A. Ex.: package primeiro; class A { protected int i; protected void f() { System.out.println(“Classe A – metodo protected”); } } package segundo; import primeiro.*; class B extends A { public static void main(String args []) throws java.io.IOException { A a = new A(); B b = new B(); b.i = 5; b.f(); // a.i = 5; ilegal // a,f(); ilegal System.in.read(); } } Encapsulamento O encapsulamento, característico da programação orientada por objectos, consiste: • na inclusão de dados e métodos dentro de classes, e • no controlo do acesso aos membros da classe. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 43 O controlo do acesso estabelece o que o programador cliente pode usar separando-o das estruturas e mecanismos internos, isto é, separa o interface da implementação. Inicialização de um objecto de uma subclasse Quando se cria um objecto de uma subclasse (classe derivada) ele contém dentro dele um sub-objecto da classe base. Este sub-objecto contém o mesmo conteúdo que teria se se criasse um objecto da classe base. Para que o sub-objecto da classe base seja inicializado correctamente Java automaticamente insere uma chamada a um construtor da classe base na sublasse (um construtor da classe base tem o conhecimento e privilégios apropriados para efectuar essa inicialização). Se o construtor da classe base não tem uma invocação explícita do construtor da superclasse, Java automaticamente chama o construtor por omissão (sem argumentos) da superclasse. Em qualquer dos casos o sub-objecto da classe base é inicializado (com a execução de um ou mais construtores da classe base) antes dos construtores da subclasse acederem ao objecto. Ex.: class A { B b1 = new B(1); A() { System.out.println("Construtor A()"); } } class B { B(int i) { System.out.println("Construtor B("+i+")"); } } class SubA extends A { B b2 = new B(2); SubA() { System.out.println("Construtor SubA()"); } } public class Sub1 { public static void main(String args[]) { SubA sa = new SubA(); } } Saída produzida pelo programa: Construtor B(1) Construtor A() Construtor B(2) Construtor SubA() DEI - ISEP Fernando Mouta 44 Programação Orientada por Objectos Ex.: class A { A(int i) { System.out.println("Construtor A("+i+")"); } } class B { B(int i) { System.out.println("Construtor B("+i+")"); } } class SubA extends A { B b1, b2 = new B(2); SubA(int i) { super(i); b1 = new B(i); } } public class Sub2 { public static void main(String args[]) { SubA sa = new SubA(1); } } Saída produzida pelo programa: Construtor A(1) Construtor B(2) Construtor B(1) Ex: class Pao { Pao() { System.out.println("Pao()"); } } class Queijo { Queijo() { System.out.println("Queijo()"); } } class Alface { Alface() { System.out.println("Alface()"); } } class Refeicao { Refeicao() { System.out.println("Refeicao()"); } } class Almoco extends Refeicao { Almoco() { System.out.println("Almoco()"); } } class Sandwich extends Almoco { Pao p = new Pao(); Queijo q = new Queijo(); Alface a = new Alface(); Sandwich() { System.out.println("Sandwich()"); } public static void main(String args []) throws java.io.IOException { new Sandwich(); System.in.read(); Fernando Mouta DEI - ISEP Programação Orientada por Objectos 45 } } Saída produzida pelo programa: Refeicao() Almoco() Pao() Queijo() Alface() Sandwich() Sumariando o processo de inicialização: Carregamento da classe: • Inicialização estática da classe base. • Inicialização estática da classe derivada. Se um objecto da subclasse é criado: • Todos os campos de dados (da classe base e da subclasse) são colocados nos valores por omissão (0’s binários); • Inicializações dos campos de dados da classe base; • Chamada do construtor da classe base (o construtor da classe base invocado explicitamente na subclasse ou o construtor por omissão); • Inicializações dos campos de dados da classe derivada; • Execução do resto do construtor da classe derivada; Ex.: class Flor { Flor(int i) { System.out.println("Flor(" + i + ")"); } void cor(int i) { System.out.println("cor(" + i + ")"); } } class Jarra { Flor f1 = new Flor(1); static Flor f2 = new Flor(2); Jarra() { System.out.println("Jarra()"); f3.cor(3); } static Flor f3 = new Flor(3); } public class InicializacaoEstatica { public static void main(String[] args) { System.out.println("No main"); new Jarra(); //(2) Jarra.f2.cor(2); //(2) } static Jarra j = new Jarra(); //(1) } DEI - ISEP Fernando Mouta 46 Programação Orientada por Objectos Saída produzida pelo programa: | Flor(2) | Flor(3) | Flor(1) | Jarra() | cor(3) | No main | Flor(1) | Jarra() | cor(3) | cor(2) | Sem (1): | | | Flor(2) | Flor(3) | No main | Flor(1) | Jarra() | cor(3) | cor(2) | Sem | (1) | e (2) | | | | | | No | main A palavra-chave final A palavra-chave final significa que não pode ser alterado. Para campos de dados: static final tipo var um único armazenamento que não pode ser mudado. final tipo-primitivo var – constante. final tipo-objecto var – esta referência não pode ser alterada para apontar para outro objecto, mas o objecto pode ser modificado. Em Java não é possível impor que um objecto seja uma constante. Todos os campos de dados declarados “final” ou são inicializados quando declarados ou então têm de ser inicializados antes de serem usados (blank finals). Para argumentos de métodos:dentro do método não podem ser mudados os valores dos argumentos. Ex.: int f(final int i) { return i+1; } Para métodos: Para classes: Fernando Mouta uma classe que herde um método final não pode mudar o seu significado (não o pode reescrever). Métodos private numa classe são implicitamente final. não permite criar subclasses desta classe. DEI - ISEP Programação Orientada por Objectos 47 Exemplos de classes em Java 1. Classe Leitura // Ficheiro Leitura.java import java.io.*; public class Leitura { public static String lerString(String msg) throws IOException { System.out.print(msg); BufferedReader d=new BufferedReader( new InputStreamReader(System.in)); return d.readLine(); } public static int lerInteiro(String msg) throws IOException { int x=Integer.parseInt( lerString(msg) ); return x; } public static float lerFloat(String msg) throws IOException { float x=Float.valueOf( lerString(msg) ).floatValue(); return x; } public static double lerDouble(String msg) throws IOException { double x=Double.valueOf( lerString(msg) ).doubleValue(); return x; } public static char lerCaracter(String msg) throws IOException { return ( lerString(msg) ).charAt(0); } } 2. Classe Tempo Crie uma classe de nome "Tempo" com os atributos "hora", "minuto" e "segundo". A classe construída deve possuir as seguintes características: a) Os objectos criados devem ter qualquer atributo, quando não fornecido, inicializado a zero. Se um valor fornecido não for válido, o correspondente atributo deve ser colocado a zero. A classe só deve permitir colocar os objectos com valores válidos de hora (0..23), minuto (0..59), e segundo (0..59). b) A classe deve permitir imprimir o tempo no formato "hhmmss". c) A classe deve permitir imprimir o tempo no formato "hh:mm:ss AM (ou PM)". d) A classe deve permitir criar um objecto correspondente a um dado tempo fornecido e em seguida acrescentar-lhe 1 segundo. e) A classe deve permitir determinar a diferença entre dois tempos fornecidos. // Ficheiro Tempo.java DEI - ISEP Fernando Mouta 48 Programação Orientada por Objectos public class Tempo { private int hora; private int minuto; private int segundo; // 0 - 23 // 0 - 59 // 0 - 59 public Tempo() { hora = 0; minuto = 0; segundo = 0; } public Tempo( int h, int m, int s ) { setHora ( h ); setMinuto ( m ); setSegundo ( s ); } public void setHora( int h ) { hora = ( ( h >= 0 && h < 24 ) ? h : 0 ); } public void setMinuto( int m ) { minuto =( ( m >= 0 && m < 60 ) ? m : 0 ); } public void setSegundo( int s ) { segundo =( ( s >= 0 && s < 60 ) ? s : 0 ); } public int getHora() { return hora; } public int getMinuto() { return minuto; } public int getSegundo() { return segundo; } // Converte o tempo em String com o formato hhmmss public String tohhmmssString() { return ( hora < 10 ? "0" : "" ) + hora + ( minuto < 10 ? "0" : "" ) + minuto + ( segundo < 10 ? "0" : "" ) + segundo ; } // Converte o tempo em String com o formato standard public String toString() { return ( ( hora == 12 || hora == 0 ) ? 12 : hora % 12 ) + ":" + ( minuto < 10 ? "0" : "" ) + minuto + ":" + ( segundo < 10 ? "0" : "" ) + segundo + ( hora < 12 ? " AM" : " PM" ); } // Acrescenta 1 segundo public void tick() { segundo = (segundo + 1) % 60; if ( segundo == 0 ) { Fernando Mouta DEI - ISEP Programação Orientada por Objectos 49 minuto = (minuto + 1) % 60; if ( minuto == 0 ) hora = (hora + 1) % 24; } } public boolean maior(Tempo t) { if ( hora > t.hora ) return true; if ( hora == t.hora ) { if ( minuto > t.minuto ) return true; if ( minuto == t.minuto ) if ( segundo > t.segundo ) return true; } return false; } public int diferencaEmSegundos(Tempo t) { int s1 = hora*3600 + minuto*60 + segundo; int s2 = t.hora*3600 + t.minuto*60 + t.segundo; return s1-s2; } public Tempo diferencaEmTempo(Tempo t) { int dif = diferencaEmSegundos(t); int s = dif % 60; // segundos dif = dif / 60; int m = dif % 60; // minutos dif = dif / 60; int h = dif % 60; // horas // Tempo t1 = new Tempo(h, m, s); // return t1; return new Tempo(h, m, s); } public static Tempo lerTempo() throws java.io.IOException { int h = Leitura.lerInteiro("Horas = "); int m = Leitura.lerInteiro("Minutos = "); int s = Leitura.lerInteiro("Segundos = "); return new Tempo(h, m, s); } } 3. Classe Data Crie uma classe de nome "Data" com os atributos "ano", "mes" e "dia". A classe construída deve possuir as seguintes características: a) Os objectos criados devem ter os atributos somente com valores válidos ( ano: qualquer valor; mes: 1..12; dia: 1..31 mas dependentes do mes e do ano). Se um DEI - ISEP Fernando Mouta 50 Programação Orientada por Objectos b) c) d) e) f) Nota: valor fornecido não for válido, o correspondente atributo deve ser colocado a 1, e deverá ser dada a correspondente mensagem. Permitir imprimir o conteúdo de um objecto no formato "aa/mm/dd". Permitir imprimir o conteúdo de um objecto por extenso. Ex.: "24 de Setembro de 2001". Determinar se uma data é mais recente que outra. Determinar o dia da semana de uma data (tome como referência o dia 1/1/1, Segunda-feira; um ano não bissexto avança um dia da semana (365 % 7 = 1) e um ano bissexto avança 2 dias da semana (366 % 7 = 2). Determinar a diferença entre duas datas. Anos bissextos são os anos divisíveis por 4 mas não por 100 ou os divisíveis por 400. // Ficheiro Data.java class Data { private int ano; private int mes; private int dia; // private String diaDaSemana; // qualquer ano // 1-12 // 1-31 dependente do mês // nome do dia da semana private static int [] diasPorMes = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; private static String [] nomeDiaSemana = { "Segunda-feira", "Terca-feira", "Quarta-feira", "Quinta-feira", "Sexta-feira", "Sabado", "Domingo" }; private static String [] nomeMes = { "Invalido", "Janeiro", "Fevereiro", "Marco", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro","Outubro", "Novembro", "Dezembro" }; // Construtor: Confirma o valor do mes e chama o metodo // validaDia para confirmar o valor do dia. public Data( int a, int m, int d ) { ano = a; // tambem poderia validar if ( m > 0 && m <= 12 ) mes = m; // valida o mês else { mes = 1; System.out.println( "Mes "+ m +" invalido. Colocado o mes 1."); } dia = validaDia( d ); // valida o dia diaDaSemana = detDiaDaSemana(); } // Confirma o valor do dia baseado no mes e ano. private int validaDia( int d ) { if ( d > 0 && d <= diasPorMes[ mes ] ) return d; Fernando Mouta DEI - ISEP Programação Orientada por Objectos 51 // se Fevereiro e dia 29 verifica se é ano bissexto if ( mes == 2 && d == 29 && anoBissexto(ano) ) return d; System.out.println( "Dia " + d + " invalido. Colocado o dia 1." ); return 1; // Deixa o objecto num estado consistente } private static boolean anoBissexto(int a) { //Ano bissexto. return ( a % 400 == 0 || a % 4 == 0 && a % 100 != 0 ) ; } private int diasDoAno() { // Número de dias desde 1 de Jan. int n=0; for (int i=1; i<=mes-1; ++i) n = n + diasPorMes[i]; if (mes > 2 && anoBissexto(ano)) ++n; return dia-1+n; } private int diaDaSemana1Jan() { // Referência: 1/1/1 = Segunda-feira (0) // um ano não bissexto avança um dia: 365%7=1 // um ano bissexto avança 2 dias: 366%7=2 int d = 0; for (int a=1; a<ano; ++a) d += anoBissexto(a)?2:1; return d%7; } private String detDiaDaSemana() { int n = diaDaSemana1Jan() + diasDoAno(); return nomeDiaSemana[n%7]; } // Cria uma String da forma dia/mes/ano public String toString() { return diaDaSemana + ", " + dia + "/" + mes + "/" + ano; } public boolean maisRecente(Data d) { if ( ano > d.ano ) return true; if ( ano == d.ano ) { if ( mes > d.mes ) return true; if ( mes == d.mes ) if ( dia > d.dia ) return true; } return false; } public long diferencaEmDias(Data d) { if ( maior(d) ) return difDias(d); return d.difDias(this); } DEI - ISEP Fernando Mouta 52 Programação Orientada por Objectos private long difDias(Data d) { if (ano == d.ano) return diasDoAno() - d.diasDoAno(); // Numero de dias até ao fim do ano da menor das datas long dias = 365 + (anoBissexto(d.ano)?1:0) - d.diasDoAno(); // Diferença de anos em dias for (int a=d.ano+1; a<ano; ++a) dias += 365 + (anoBissexto(a)?1:0) ; // Numero de dias desde 1 Jan. até à maior das datas return dias + diasDoAno(); } public static Data lerData() throws java.io.IOException { int a = Util.lerNumero("Ano = "); int m = Util.lerNumero("Mes = "); int d = Util.lerNumero("Dia = "); return new Data(a, m, d); } } 4. Exercício: “Vencimentos” Construa um programa para calcular os vencimentos mensais dos seguintes tipos de trabalhadores. 1. Patrão (Patrao): vencimento fixo independente do número de horas de trabalho. 2. Trabalhador à comissão (TrabCom): vencimento igual a um salário base mais uma percentagem das vendas. 3. Trabalhador à peça (TrabPeca): vencimento proporcional ao número de peças produzidas. 4. Trabalhador à hora (TrabHora): vencimento proporcional às horas de trabalho. Construa uma classe para cada tipo de empregado com uma implementação apropriada de um método vencimento para cada classe. Construa ainda uma superclasse destas classes designada Empregado. Construa uma aplicação que crie 4 objectos, cada um representando um determinado trabalhador de cada tipo: Objecto p, do tipo Patrao, de nome Jorge Silva, com o vencimento 800.00. Objecto tc, do tipo TrabCom, de nome Susana Ferreira, com o salário base 400.00 e uma comissão 6% sobre as vendas efectuadas que totalizaram o valor de 150.00. Objecto tp, do tipo TrabPec, de nome Miguel Mendes, que ganha por peça 2.5 e com uma quantidade de peças produzidas de 200. Objecto th, do tipo TrabHora, de nome Carlos Miguel, que ganha por hora 3.0 e com um total de horas de trabalho 160. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 53 Crie um array de 4 elementos do tipo Empregado e atribua aos elementos desse array as referências dos objectos criados. Percorra os elementos do array invocando o método vencimento() para cada um e mostre os resultados. // Ficheiro Empregado.java // classe abstracta base Empregado public abstract class Empregado { private String primNome; private String ultNome; // Construtor public Empregado( String pNome, String uNome ) { primNome = pNome; ultNome = uNome; } // método abstracto que deve ser implementado nas classes // derivadas de Empregado das quais sao instanciados objectos abstract double vencimento(); public String getPrimNome() { return primNome; } public String getUltNome() { return ultNome; } public String toString() { return primNome + “ “ + ultNome; } } // Ficheiro Patrao.java //classe Patrao derivada de Empregado public class Patrao extends Empregado { private double salario; // Construtor public Patrao( String pNome, String uNome, double sal) { super( pNome, uNome ); // invoca constr. da superclasse salario = sal; } public double vencimento() { return salario; } public String toString() { return "Patrao: " + super.toString(); } } // Ficheiro TrabCom.java // classe TrabCom derivada de Empregado DEI - ISEP Fernando Mouta 54 Programação Orientada por Objectos public class TrabCom extends Empregado { private double salario; // salario base private double comissao; // percentagem das vendas private double quantidade; // total de vendas // Construtor public TrabCom( String pNome, String uNome, double sal, double com, double quant) { super( pNome, uNome ); // invoca constr. da superclasse salario = sal; comissao = com; quantidade = quant; } public double vencimento() { return salario + comissao * quantidade; } public String toString() { return "Trabalhador Comissao: " + super.toString(); } } // Ficheiro TrabPeca.java // classe derivada de Empregado public class TrabPeca extends Empregado { private double pagPeca; // pagamento por peca private int quantidade; // quantidade de pecas // Construtor public TrabPeca( String pNome, String uNome, double pp, int quant ) { super( pNome, uNome ); // invoca constr. da superclasse pagpeca = pp; quantidade = quant; } public double vencimento() { return quantidade * pagPeca; } public String toString() { return "Trabalhador a Peca: " + super.toString(); } } // Ficheiro TrabHora.java // classe derivada de Empregado public class TrabHora extends Empregado { private double pagHora; // pagamento por hora private double horas; // horas de trabalho // Construtor public TrabHora ( String pNome, String uNome, double ph, double hs ) { super( pNome, uNome ); // invoca constr. da superclasse Fernando Mouta DEI - ISEP Programação Orientada por Objectos 55 pagHora = ph; horas = hs; } public double vencimento() { return pagHora * horas; } public String toString() { return "Trabalhador a Hora: " + super.toString(); } } // Ficheiro Teste.java // classe Teste public class Teste { public static void main(String args []) throws java.io.IOException { Patrao p = new Patrao(“Jorge”, “Silva”, 800.00); TrabCom tc = new TrabCom(“Susana”, “Ferreira”, 400.0, 0.06, 150.0); TrabPeca tp = new TrabPeca(“Miguel”, “Mendes”, 2.5, 200); TrabHora th = new TrabHora(“Carlos”, “Miguel”, 3.0, 160); Empregado [] a = new Empregado[4]; for (int i=0; i<a.length; i++) System.out.println( a[i] + “ tem o vencimento de “ + a[i].vencimento() ); // Listagem apenas dos trabalhadores a hora for (int i=0; i<a.length; i++) if (a[i] instanceof TrabHora) System.out.println( a[i] ); } } 5. Exercício “Frota de Veículos” Uma empresa dispõe de uma frota de veículos motorizados para os quais se pretende gerir as revisões periódicas necessárias para uma boa conservação dos mesmos. Estes veículos caracterizam-se por marca, ano de matrícula, matrícula, quilómetros efectuados até ao momento e quilómetros marcados à data da última revisão. Os tipos de viaturas pertencentes à empresa dividem-se em ligeiros, motociclos e pesados. Os primeiros além das características apontadas devem ainda indicar o números de passageiros e a periodicidade de revisão. Esta deverá ser de 5000 em 5000 Km. No caso dos motociclos a periodicidade de revisão será de 1000 em 1000 km e nos pesados de 15000 em 15000 km. Estes últimos têm ainda mais um atributo que é o número de eixos. DEI - ISEP Fernando Mouta 56 Programação Orientada por Objectos a) Defina em Java as classes necessárias (atributos e construtores) que permitam modelizar a empresa atendendo apenas à frota de veículos. b) Construa método(s) que mostrem as matrículas das viaturas que necessitam de revisão. c) Implemente os métodos que permitam listar as características do veículo da empresa que no momento apresenta maior número de quilómetros. // Ficheiro Veiculo.java public abstract class Veiculo { private String marca; private int anoMatricula; private String matricula; private int kmEfectuados; private int kmUltRevisao; public Veiculo( String mar, int aM, String mat) { marca = mar; anoMatricula = aM; matricula = mat; } public void setKmEfectuados(int km) { kmEfectuados = km; } public void setKmUltRevisao(int km) { kmUltRevisao = km; } public abstract int getPeriodicidade(); // { return periodicidade; } public boolean necessitaRevisao() { if ( kmEfectuados >= kmUltRevisao + getPeriodicidade() ) return true; else return false; } public String getMatricula() { return matricula; } public int getKmEfectuados() { return kmEfectuados; } public String toString() { return "Marca: " + marca + "\nAno de matricula: " + anoMatricula + "\nKm efectuados: " + kmEfectuados + "\nKm na ultima revisao: " + kmUltRevisao; } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 57 // Ficheiro Ligeiro.java public class Ligeiro extends Veiculo { private static int periodicidade = 5000; private int numeroPassageiros; public Ligeiro( String mar, int aM, String mat, int numP) { super(mar, aM, mat); numeroPassageiros = numP; } public int getPeriodicidade() { return periodicidade; } public String toString() { return super.toString() + "\nPeriodicidade: " + periodicidade + "\nNumero de passageiros: " + numeroPassageiros; } } // Ficheiro Motociclo.java public class Motociclo extends Veiculo { private static int periodicidade = 1000; public Motociclo( String mar, int aM, String mat ) { super(mar, aM, mat); } public int getPeriodicidade() { return periodicidade; } public String toString() { return super.toString() + "\nPeriodicidade: " + periodicidade; } } // Ficheiro Pesado.java public class Pesado extends Veiculo { private static int periodicidade = 15000; private int numeroEixos; public Pesado( String mar, int aM, String mat, int numE) { super(mar, aM, mat); numeroEixos = numE; } public int getPeriodicidade() { return periodicidade; } public String toString() { return super.toString() + "\nPeriodicidade: " + periodicidade + "\nNumero de eixos: " + numeroEixos; } } DEI - ISEP Fernando Mouta 58 Programação Orientada por Objectos // Ficheiro Teste.java public class Teste { public static void main (String [] args) throws java.io.IOException { Veiculo [] v = new Veiculo [10]; v[0]= new Ligeiro("Ford", 1998, "77-44-AM", 5); v[0].setKmEfectuados(90000); v[0].setKmUltRevisao(1500); v[1]= new Motociclo("BMW", 1999, "60-60-BX"); v[1].setKmEfectuados(8000); v[1].setKmUltRevisao(2200); v[2]= new Pesado("Fiat", 1997, "50-50-AX", 4); v[2].setKmEfectuados(30000); v[2].setKmUltRevisao(8000); for (int i=0; i<v.length; ++i) { if (v[i] != null) if (v[i].necessitaRevisao()) System.out.println(v[i].getMatricula()); } Veiculo v1 = v[0]; int maiorNumKm = v[0].getKmEfectuados(); for (int i=0; i<v.length; ++i) { if (v[i] != null) if (v[i].getKmEfectuados() > maiorNumKm) { maiorNumKm = v[i].getKmEfectuados(); v1 = v[i]; } } System.out.println( "\nVeiculo com maior numero de Kms: " + v1); System.in.read(); } } /* Resultados: 77-44-AM 60-60-BX 50-50-AX Veiculo com maior numero de Kms: Marca: Ford Ano de matricula: 1998 Km efectuados: 90000 Km na ultima revisao: 1500 Periodicidade: 5000 Numero de passageiros: 5 */ Fernando Mouta DEI - ISEP Programação Orientada por Objectos 59 6. Exercício “Loja de Equipamento Informático” Considere a necessidade de desenvolver uma aplicação para uma loja de equipamento informático que comercializa computadores, impressoras e modems. Todos os equipamentos possuem uma referência única, uma marca, um modelo, um preço de custo e um preço de venda ao público. Para além destas características globais os computadores têm como atributo adicional o tipo de processador, as impressoras o tipo (laser, jacto de tinta ou matriz de pontos) e os modems a localização (internos ou externos). As impressoras de matriz de pontos e os modems externos encontram-se em promoção. a) Escreva, em Java, as classes necessárias com os atributos e construtores que julgar convenientes. b) O preço de venda ao público é calculado através da aplicação da taxa de IVA (17%) e da margem de lucro ao preço de custo. A margem de lucro dos produtos em promoção é de 5% e dos restantes é 20%. Escreva métodos para calcular o preço de venda ao público de um qualquer equipamento. // Ficheiro Equipamento.java public class Equipamento { private static int numId = 0; private int referencia; private String marca; private String modelo; private double precoCusto; private double precoVenda; private boolean promocao; public Equipamento() { referencia = numId++; precoVenda = 0; } public Equipamento( String mar, String mod, double pC ) { this(); marca = mar; modelo = mod; precoCusto = pC; } public boolean estaEmPromocao() { return false ; } public void setPrecoVenda() { double margemLucro; if (estaEmPromocao()) margemLucro=0.05; else margemLucro=0.2; precoVenda= precoCusto * (1 + margemLucro) * 1.17; } public double getPrecoVenda() { if (precoVenda == 0) setPrecoVenda(); return precoVenda; } public String toString() { DEI - ISEP Fernando Mouta 60 Programação Orientada por Objectos return "\nReferencia: " + referencia + "\nMarca: " + marca + "\tModelo: " + modelo + "\nPreco de custo: " + precoCusto + "\tPreco de venda: " + precoVenda; } } // Ficheiro Computador.java public class Computador extends Equipamento { private String tipoProcessador; public Computador( String mar, String mod, double pC, String tipo) { super(mar, mod, pC); tipoProcessador = tipo; } public boolean estaEmPromocao() { return false; } public String toString() { return super.toString() + "\nTipo de processador: " + tipoProcessador + "\n" + (estaEmPromocao()? "Esta ": "Nao esta ") + "em promocao."; } } // Ficheiro Impressora.java public class Impressora extends Equipamento { // LASER, JACTO_DE_TINTA, MATRIZ_DE_PONTOS private int tipo; public static final int LASER = 0; public static final int JACTO_DE_TINTA = 1; public static final int MATRIZ_DE_PONTOS = 2; private static boolean [] impressorasPromocao = {false, false, false}; public Impressora( String mar, String mod, double pC, int t) { super(mar, mod, pC); tipo = t; } public static void setImpressorasPromocao(int tipo) { impressorasPromocao[tipo]= true; } public static void setImpressorasSemPromocao(int tipo) { impressorasPromocao[tipo]= false; } public boolean estaEmPromocao() { return impressorasPromocao[tipo]; } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 61 public String toString() { return super.toString() + "\nTipo: " + tipo + "\n" + (estaEmPromocao()? "Esta ": "Nao esta ") +"em promocao."; } } // Ficheiro Modem.java public class Modem extends Equipamento { private int localizacao; // INTERNO, EXTERNO public static final int INTERNO = 0; public static final int EXTERNO = 1; private static boolean [] modemsPromocao = {false, false}; public Modem( String mar, String mod, double pC, int loc) { super(mar, mod, pC); localizacao = loc; } public static void setModemsPromocao(int tipo) { modemsPromocao[tipo]= true; } public static void setModemsSemPromocao(int tipo) { modemsPromocao[tipo]= false; } public boolean estaEmPromocao() { return modemsPromocao[localizacao]; } public String toString() { return super.toString() + "\nLocalizacao: " + localizacao + "\n" + (estaEmPromocao()? "Esta ": "Nao esta ") + "em promocao."; } } // Ficheiro Teste.java public class Teste { public static void main (String [] args) throws java.io.IOException { Equipamento [] eq = new Equipamento [10]; eq[0]= new Computador("IBM", "Pentium", 250, "Pentium III"); eq[1]= new Impressora("HP", "DeskJet", 40, 2); eq[2]= new Impressora("HP", "DeskJet", 40, Impressora.LASER); eq[3]= new Modem("HP", "55", 10, 1); Impressora.setImpressorasPromocao( Impressora.MATRIZ_DE_PONTOS); Modem.setModemsPromocao(Modem.EXTERNO); for (int i=0; i<eq.length; ++i) { if (eq[i] != null) { System.out.println(eq[i]); System.out.println(eq[i].getPrecoVenda()); } DEI - ISEP Fernando Mouta 62 Programação Orientada por Objectos } System.in.read(); } } /* Resultados: Referencia: 0 Marca: IBM Modelo: Pentium Preco de custo: 250 Preco de venda: 0.0 Tipo de processador: Pentium III Nao esta em promocao. 351.0 Referencia: 1 Marca: HP Modelo: DeskJet Preco de custo: 40.0 Preco de venda: 0.0 Tipo: 2 Esta em promocao. 49.14 Referencia: 2 Marca: HP Modelo: DeskJet Preco de custo: 40.0 Preco de venda: 0.0 Tipo: 0 Nao esta em promocao. 56.16 Referencia: 3 Marca: HP Modelo: 55 Preco de custo: 10.0 Preco de venda: 0.0 Localizacao: 1 Esta em promocao. 12.285 */ Fernando Mouta DEI - ISEP Programação Orientada por Objectos 63 UML UML – Unified Modeling Language – é um formalismo de representação de sistemas computacionais, através de diagramas. Ajuda a capturar a visão inicial de um sistema, comunicá-la e mantê-la durante o projecto e criação do sistema. UML consiste de um dado número de elementos gráficos que combinados, segundo determinadas regras, formam diagramas. O objectivo dos diagramas é apresentar várias vistas de um sistema. O conjunto dos diagramas de um sistema, designados por modelo UML do sistema, descrevem o que o sistema deve realizar e não como deve ser implementado. UML define vários tipos de diagramas. Vamos apenas descrever os diagramas de classes. Diagramas de classes em UML Um sistema computacional para representar e interactuar com o mundo necessita de simular e manipular várias entidades e aceder às suas características. Podemos distinguir dois tipos dessas características: atributos e comportamentos. A organização das várias entidades envolvidas leva às categorias a que cada entidade naturalmente pertence. Estas categorias designam-se por classes. Uma classe é um conjunto de entidades que têm atributos e comportamentos semelhantes. Um diagrama de classes é um diagrama que mostra classes, interfaces e suas relações. Classes Uma classe é representada por um rectângulo dividido em 2 ou 3 partes. Na parte superior coloca-se o nome da classe, na parte do meio os atributos (campos ou variáveis), e na inferior os comportamentos (métodos). Se uma classe é representada apenas por 2 compartimentos, as variáveis, embora possam existir, não são mostradas.Se é representada apenas por um compartimento só contém o nome da classe. Cada variável ou método é precedida de um símbolo que indica a visibilidade: + public # protected private e é seguida de “: Tipo” para indicar o tipo da variável ou do retorno do método, excepto se um método retorna “void”, caso em que não se coloca. DEI - ISEP Fernando Mouta 64 Programação Orientada por Objectos Exemplo: Classe1 -campo1: Tipo = valor1 -campo2: Tipo «constructor» +Classe1() «misc» +metodo1(var1:Tipo1, var2:Tipo2):Tipo3 +metodo2() Um valor inicial para uma variável é indicado a seguir ao tipo. Os parâmetros formais dos métodos consistem do nome e tipo, separados por vírgulas. Num diagrama UML palavras entre aspas como «constructor» designam-se por estereótipos e são usados para qualificar o que segue. O esterótipo «constructor» indica construtores e o estereótipo «misc» indica métodos normais. Classes ou métodos abstractos são escritos em itálico. Interfaces Os interfaces são desenhados de um modo semelhante às classes, com a excepção do nome, no compartimento superior, que é precedido pelo estereótipo «interface». Relações entre classes e interfaces Uma linha contínua com uma seta fechada indica a relação de herança entre uma subclasse e a sua superclasse. Uma linha ponteada ou tracejada com uma seta fechada indica que uma classe implementa um interface. Uma linha contínua com uma seta aberta indica uma relação entre classes e/ou interfaces designada por associação. Opcionalmente a meio da linha que representa a associação pode aparecer um nome para a associação (com inicial maiúscula) com a indicação do sentido em que deve ser lida essa associação. A seta na extremidade da linha que representa a associação indica o sentido dessa associação. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 65 «interface» ClasseA Interface1 ClasseB ClasseC Para clarificar a natureza da associação, o nome do papel que cada classe desempenha na associação pode aparecer ( em minúsculas) nas extremidades da linha que representa a associação, ao lado da correspondente classe. Normalmente também se indica a quantidade de instâncias de cada classe que participam numa ocorrência de uma associação. Este indicador de multiplicidade pode aparecer de cada lado de uma associação e pode consistir em: um simples número, por ex. 0 ou 1 uma gama de números, por ex. 0..2 0 .. *, ou *, significando qualquer número de instâncias 1 .. *, significando pelo menos 1 instância. Uma classe com múltiplas subclasses pode ser desenhada como segue: ClasseA ClasseB DEI - ISEP ClasseC ClasseD Fernando Mouta 66 Programação Orientada por Objectos Sintaxe da linguagem Java Identificadores Identificadores em Java usados para nomes de entidades declaradas tais como variáveis, constantes, nomes de classes ou métodos Têm que começar por: uma letra, _ ou $, seguida de letras, dígitos ou ambos. Os identificadores são “case sensitive”: upper case ≠ lower case Ex.: soma ≠ Soma As palavras-chave da linguagem não podem ser utilizadas como identificadores: abstract boolean break byte case catch char class const continue default do double else extends final finally float for goto if implements import instanceof int interface long native new package private protected public return short static super switch synchronized this throw throws transient try void volatile while Também não podem ser usados como identificadores os literais: null, true e false. Tipos de dados: Tipos primitivos de dados: contêm. um valor único: inteiro, real, carácter ou booleano Tipos referência: contêm uma referência para um objecto: arrays, classes ou interfaces. Todas as variáveis têm que ser declaradas antes de serem usadas e devem ser de um tipo de dados. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 67 O tipo de dados determina os valores que a variável pode ter e as operações que lhe podem ser aplicadas. Tipos primitivos de dados: n.os inteiros entre n.os inteiros entre n.os inteiros entre aprox. n.os inteiros entre aprox. n.os reais precisão simples n.os reais precisão dupla byte short int long float double 8 bits 16 bits 32 bits 64 bits 32 bits 64 bits –128 e 127 –32768 e 32767 –2x108 e 2x108 –9x1018 e 9x1018 (1.4E-45 a 3.4E+38) (4.9E-324 a 1.7E+308) char boolean 16 bits caracteres em Unicode 1 bit pode ter o valor true ou false Unicode – código de caracteres internacional standard capaz de representar a maior parte das línguas do mundo escritas. Os ambientes existentes de Java lêem ASCII e convertem em Unicode. Os primeiros 256 caracteres do Unicode são o conjunto de caracteres Latinos. Como poucos editores de texto suportam caracteres Unicode, Java reconhece sequências escape \udddd, onde cada d é um dígito hexadecimal. Literais São os valores que as variáveis podem ter 4 tipos de literais: numéricos de caracteres de string lógicos ou booleanos Literais numéricos: Ex.: 178 n.º no sistema decimal 023 n.º no sistema octal 0x5A n.º no sistema hexadecimal 0.25 n.º real 2e5 n.º real 2E-22 n.º real Constantes inteiras são cadeias de dígitos octais, decimais ou hexadecimais. O início da constante determina a base do número: • 0 (zero) denota octal, • 0x ou 0X denota hexadecimal • e qualquer outro conjunto de dígitos é assumido como decimal. Uma constante inteira é do tipo long se termina em l ou L (L é preferido porque l confunde-se com 1). Senão é assumida como sendo do tipo int. DEI - ISEP Fernando Mouta 68 Programação Orientada por Objectos Se um literal int é atribuído directamente a uma variável short ou byte e se o seu valor está dentro da gama de valores válidos, o literal inteiro é convertido no respectivo tipo short ou byte. Se a atribuição não é directa é necessário explicitar o casting. Números em vírgula flutuante são expressos com números decimais com ponto decimal e expoente opcionais. Constantes de vírgula flutuante são assumidas como double, a não ser que terminem por f ou F, o que as torna float. Se terminam por d ou D especificam uma constante double. Uma constante double não pode ser atribuída directamente a uma variável float, mesmo que o valor esteja dentro da gama de valores válidos. Literais de caracteres: São expressos por um carácter único dentro de pelicas Ex.: ‘a’ ‘x’ ‘8’ Alguns literais de caracteres representam caracteres que não são impressos ou acessíveis através do teclado – sequências escape: \n \t \b \r \\ newline tab backspace carriage return backslash Literais de string: Cadeias de caracteres inserida entre aspas. Podem conter sequências escape. Ex.: String s1 = “Nome \tEndereço”; Em Java uma string é um objecto e não é armazenada num array com em C. Quando um literal de string é usado, Java armazena esse valor como um objecto String, sem termos de criar explicitamente um novo objecto, como é necessário com outros objectos. Como strings são objectos, existem métodos para combinar strings, modificar strings e determinar se duas strings têm o mesmo valor. Literais booleanos: São os valores true e false. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 69 Variáveis Localização de memória, utilizada durante a execução de um programa, para conter informação. Cada variável possui 3 componentes: Nome Tipo de dados Valor Nome: case sensitive, começa por letra, _ ou $. Tipo de dados: byte, short, int, long, float, double, char, boolean, array, ou objecto. Variáveis têm de ser declaradas antes de ser usadas. Uma variável declarada numa classe (membro de uma classe) é automaticamente inicializada a: 0 variáveis numéricas ‘\0’ caracteres false booleanos null (referência a) objectos Mas uma variável local (declarada num método) deve ser inicializada explicitamente antes de ser usada num programa, senão dá erro de compilação. Variáveis locais podem ser declaradas em qualquer lugar dentro de um método. Ex.: int a; int b,c,d; int a = 10, b = 12; Atribuição de valores a variáveis: a = b = c = 2; Constantes Tipo especial de variável cujo valor nunca muda durante a execução do programa. A declaração tem de ser precedida da palavra-chave final e incluir a atribuição de um valor a essa variável. Ex.: final int max = 10; final double taxa = 0.17; Scope (âmbito de validade) de uma variável Determina quer a visibilidade quer o tempo de vida da variável DEI - ISEP Fernando Mouta 70 Programação Orientada por Objectos Ex.: { int x = 10; // só x está disponível { int y = 20; // x e y estão ambos disponíveis } // só x está disponível, y fora de scope } Em Java não é permitido o seguinte (legal em C e C++): { int x = 10; { int x = 20; } } Resulta um erro ao compilar. Todas as instruções terminam por ; excepto a instrução composta ou bloco de instruções: { instr1; instr2; instr3; } Comentários: /* ... */ // /** ... */ início e fim de comentário; o comentário pode estender-se por mais que uma linha. tudo à direita deste símbolo até ao fim da linha é comentário. início e fim de comentário utilizado para gerar documentação (javadoc). Casting Java é uma linguagem fortemente tipada que verifica sempre que possível a compatibilidade de tipos em tempo de compilação. Pode ser necessário realizar conversão de um tipo de dados noutro: • em operações de atribuição de um valor a uma variável; • em operandos dentro de expressões; • ou quando se usam valores como parâmetros de métodos. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 71 Alguns tipos de conversão são feitos automaticamente (implicitamente) enquanto que outros têm de ser forçados explicitamente (quando a compatibilidade de um tipo só pode ser determinada em tempo de execução ou quando se pretende efectuar a conversão entre tipos primitivos de dados que diminuem a gama de valores representáveis). Java permite efectuar o casting de qualquer tipo primitivo de dados para outro qualquer tipo primitivo de dados excepto boolean. Conversão implícita Quando é necessário realizar uma conversão, de um tipo primitivo de dados para outro tipo primitivo de dados que suporte uma maior gama de valores, não é necessário explicitar o casting - geralmente não há perda de informação. Java efectua a conversão implícita de tipos inteiros em tipos de vírgula flutuante, mas não o inverso. Não há perda na gama de valores representáveis quando se passa de long para float, mas nesta conversão implícita pode-se perder precisão, porque um float tem 32 bits enquanto que um long tem 64 bits. Um char pode ser usado onde um int seja válido Conversão explícita Quando é necessário realizar uma conversão de um tipo de dados para outro tipo de dados que apenas suporte uma menor gama de valores temos de efectuar o casting explicitamente porque se corre o risco de perder informação. ( tipo ) ( expressão ) Ex.: int x = (int) (y/0.1); Quando um número em vírgula flutuante (float ou double) é convertido num inteiro, a parte fraccional é cortada. Na conversão entre inteiros os bits mais significativos são cortados. DEI - ISEP Fernando Mouta 72 Programação Orientada por Objectos Operadores Operadores aritméticos + * / % adição subtracção multiplicação divisão ( se entre inteiros produz resultados inteiros ) módulo ( resto da divisão inteira ). A divisão inteira trunca o resultado, não arredonda. Operadores abreviados de atribuição: a = a + b; a = a - b; a = a * b; a = a / b; a += b; a -= b; a *= b; a /= b; Incrementar o valor de uma variável: i = i + 1; ++ i; i ++; Em qualquer uma destas notações o valor da variável é incrementado, mas: • Se o operador ++ é colocado antes da variável,o valor da variável usado na instrução é o novo valor depois de incrementado. • Se o operador ++ é colocado depois da variável, o valor da variável usado na instrução é o valor antes de incrementado. Ex.: b = 3; a = ++b; Depois da execução: a = 4, b = 4. b = 3; a = b++; Depois da execução: a = 3, b = 4. Decrementar o valor de uma variável: i = i - 1; -- i i -- Fernando Mouta -- i; i --; primeiro decrementa e depois usa i. primeiro usa i e depois decrementa. DEI - ISEP Programação Orientada por Objectos 73 Operadores Bitwise: ~ & | ^ inversão – operador unário and – operador binário or – operador binário exclusive-or – operador binário Estes operadores só realizam operações em valores inteiros. O operador inversão inverte os bits que constituem o valor inteiro, e os outros operadores realizam a respectiva operação ao nível do bit: o bit de ordem n do resultado é calculado usando os bits de ordem n de cada operando. Valores byte, short, e char são convertidos em int antes de uma operação bitwise ser aplicada. Ainda se um operador bitwise binário tem um operando long, o outro é convertido para long antes da operação. Operadores Shift (operadores binários): << left shift >> right shift >>> unsigned right shift Estes operadores causam o desvio dos bits do operando da esquerda o número de vezes especificado no outro operando. Valores byte, short, e char do operando esquerdo são convertidos em int antes da operação ser aplicada. Se o operando esquerdo é um int só os últimos 5 bits do operando direito são considerados (num int, 32 bits, só se pode efectuar desvios 32 vezes); se o operando esquerdo é long só os últimos 6 bits do operando direito são considerados (num long, 64 bits, só se pode efectuar desvios 64 vezes). O operador << efectua o desvio para a esquerda sendo os bits à direita cheios com 0´s. O operador >> efectua o desvio para a direita sendo os bits à esquerda cheios com o valor do bit mais à esquerda antes da operação. O operador >>> efectua o desvio para a direita sendo os bits à esquerda cheios com 0´s. Operadores lógicos Os operadores lógicos AND, OR, XOR e NOT produzem um valor booleano baseado na relação lógica dos seus argumentos. Só podem ser aplicados a valores booleanos. AND (E) expr1 && expr2 ou expr1 & expr2 verdadeiro (true) se e só se expr1 e expr2 são verdadeiras. expr1 && expr2: expr1 & expr2: DEI - ISEP se expr1 é falsa, expr2 já não é avaliada. ambas as expressões são avaliadas. Fernando Mouta 74 Programação Orientada por Objectos OR (OU) expr1 || expr2 expr1 || expr2: expr1 | expr2: ou expr1 | expr2 verdadeiro (true) se expr1 ou expr2 é verdadeira. se expr1 é verdadeira, expr2 já não é avaliada. ambas as expressões são avaliadas. XOR (OU Exclusivo) expr1 ^ expr2 verdadeiro se apenas uma das expressões é verdadeira. ! expr verdadeiro se expr é falsa, e falsa se expr é verdadeira. NOT Operadores relacionais (de comparação) Os operadores relacionais avaliam a relação entre os valores dos operandos e produzem um valor booleano. == != < > <= >= igual diferente menor que maior que menor ou igual a maior ou igual a instanceof determina se uma referência a um objecto (o operando esquerdo) é uma instância da classe, interface ou tipo de array especificado no operando direito. Operador ternário Condicional ? : O operador condicional ? : tem 3 operandos. valor = ( exprBool ? expr0 : expr1 ) ; Se exprBool é verdadeira o operador produz o valor de expr0, senão produz o valor de expr1. if ( exprBool ) valor = expr0; else valor = expr1; A diferença principal entre o operador condicional e a instrução if é que o operador condicional produz um valor. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 75 Exemplo: static int menor ( int i, int j ) { return i < j ? i : j ; } static int menorAlternativo ( int i, int j ) { if (i < j) return i ; return j ; } Operador + usado para concatenar Strings O operador + pode ser usado para concatenar Strings. Se um dos operandos numa operação + é uma String então o outro operando é convertido para uma String. Ex.: int x = 1; y = 2; String s= “Valores de x e y: “; System.out.println( s + x + y ); Java possui definições de conversão para String de todos os tipos primitivos de dados. Todos os tipos primitivos de dados são impliciatmente convertidos em objectos String quando usados em expressões deste tipo, mas não noutra alturas. Por exemplo, a um método que recebe um parâmetro String deve ser-lhe passado uma String. Se o operando a converter para String é um objecto, o seu método toString() é invocado. Precedência dos operadores - ordem de avaliação das expressões Hierarquia de precedências (das mais altas para as mais baixas): ++ (postfixo), -- (postfixo) ++ (prefixo), -- (prefixo), (tipo) *, /, % + (binário), - (binário) <<, >>, >>> <, >, >=, >= ==, != & ^ | && || ?: =, +=, -=, *=, ~=, != DEI - ISEP + (unário), - (unário), ~, ! <<=, >>>=, &=, instanceof /=, %=, >>=, Fernando Mouta 76 Programação Orientada por Objectos Estruturas de Controlo do Fluxo do Programa Estruturas de decisão: 1 - A estrutura if if ( cond ) instrução; ou if ( cond ) { blocoDeInstruções } A condição cond é avaliada; se for verdadeira (true) a instrução ou bloco de instruções são executados; se for falsa (false) a instrução ou bloco de instruções são ignorados. Teste da Condição Verdadeiro Acção Falso 2 - A estrutura if . . . else if ( cond ) instrução1; else instrução2; ou if ( cond ) { blocoDeInstruções1 else { blocoDeInstruções2 } A condição cond é avaliada; se for verdadeira (true) a instrução1 ou o bloco de instruções 1 é executado; se for falsa (false) a instrução2 ou bloco de instruções 2 é executado. Fernando Mouta DEI - ISEP Programação Orientada por Objectos Falso Acção 77 Teste da Condição Verdadeiro Acção Podem-se encadear vários if . . . else if ( cond1 ) instr1; else if ( cond 2) instr2; else if ( cond 3) instr3; else instr4; Um else é sempre associado com o último if excepto se se usam chavetas para exprimir de um modo diferente. Ex.1: if ( cond1 ) if ( cond 2 ) instrA; else instrB; Ex.2: if ( cond1 ) { if ( cond 2 ) instrA; } else instrB; No Ex.1 o compilador associa o else ao segundo if enquanto que no Ex.2 o else é associado ao primeiro if. 3 - A estrutura switch A estrutura switch é usada para substituir estruturas encadeadas de cláusulas if . . . else if . . . mas só quando o valor da expressão a testar é do tipo: byte, short, int ou char. DEI - ISEP Fernando Mouta 78 Programação Orientada por Objectos switch ( expr ) { case valor1: instrução1; break; case valor2: instrução2; break; ... default: instruçãon; } A instrução break causa o abandono da estrutura switch A instrução default é opcional. Exemplo: int deci; switch ( ch ) { case ‘0’: case ‘1’: case ‘2’: case ‘3’: case ‘4’: case ‘5’: case ‘6’: case ‘7’: case ‘8’: case ‘9’: deci = ch – ‘0’; break; case ‘a’: case ‘b’: case ‘c’: case ‘d’: case ‘e’: case ‘f’: deci = ch – ‘a’ + 10; break; case ‘A’: case ‘B’: case ‘C’: case ‘D’: case ‘E’: case ‘F’: deci = ch – ‘A’ + 10; break; default: System.out.println(“Caracter nao hexadecimal”); } 4 - A estrutura for A estrutura for é usada para repetir uma instrução um número especificado de vezes até se verificar uma dada condição. for ( inicialização; teste; incremento ) instrução; ou for ( inicialização; teste; incremento ) { blocoDeInstruções } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 79 Inicialização Teste da Condição Verdadeiro Acção Incremento Falso Ex.: for (int i = 1; i < 5; i++) System.out.println(i); Na inicialização e no incremento pode haver mais do que uma instrução separadas por vírgulas, sendo neste caso estas instruções avaliadas sequencialmente. Ex.: for (int i = 0, j = i; i < 10 && j != 10 ; i++, j = i*2 ) { System.out.println( “i= “ + i + “ j= “ + j ); } 5 - A estrutura while A estrutura while é usada para que enquanto uma condição seja verdadeira, uma instrução ou um bloco de instruções sejam executados repetidamente. while ( cond ) instrucao; ou while ( cond ) { blocoDeInstruções } Ex: int i = 1; while ( i < 5 ) { System.out.println(i); i ++; } DEI - ISEP Fernando Mouta 80 Programação Orientada por Objectos Teste da Condição Verdadeiro Acção Falso 6 - A estrutura do . . . while A estrutura do . . . while é usada para repetir uma instrução ou um bloco de instruções enquanto uma condição for verdadeira. do instrução; while ( cond ); ou do { blocoDeInstruções } while ( cond ); Acção Teste da Condição Verdadeiro Falso A condição só é testada no fim. A instrução ou bloco de instruções é executado pelo menos uma vez. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 81 A instrução break A instrução break interrompe a execução de um ciclo. O programa continua executando as instruções fora desse ciclo. while (cond1) { instrA; if (cond2) break; instrB; } Se cond2 é verdadeira o ciclo é terminado mesmo que cond1 se mantenha verdadeira. A instrução break também pode ser utilizada em ciclos for e do, interrompendo a execução desses ciclos. A instrução continue A instrução continue quando incluída no bloco de instruções de um ciclo, faz com que sejam ignoradas as instruções que se lhe seguem no bloco de instruções iniciando-se um novo ciclo depois do teste da condição de ciclo no caso dos ciclos while e do, e depois do incremento e do teste no caso do ciclo for. Ex.: while ( cond1 ) { instrA; if ( cond2 ) continue; instrB; } Se cond2 for avaliada como verdadeira a instrB é ignorada e a cond1 é avaliada. Se for verdadeira o ciclo repete-se. Assim pode haver iterações em que a instrB é executada e iterações em que o não é. No caso de ciclos encadeados o efeito das instruções break e continue afecta apenas o ciclo onde estão integradas. Ex.: for (int i=1; i<5; i++) { while ( cond1 ) { instrA; while ( cond2 ) { instrX; if ( cond3 ) break; instrY; } } } DEI - ISEP Fernando Mouta 82 Programação Orientada por Objectos Se cond3 é verdadeira a instrução break cancela a execução do ciclo onde está integrada e a execução do programa continua no ciclo while (cond1) começando por testar cond1. Suponhamos que se pretendia que fosse continuado o ciclo for quando cond3 fosse verdadeira. Deve-se usar um label, e a instrução continue label. Designemos o label por retomar: Ex.: retomar: for (int i=1; i<5; i++) { while ( cond1 ) instrA; while ( cond2 ) { instrX; if ( cond3 ) continue retomar; instrY; } } } continue retomar leva a retomar a execução do ciclo for, começando pelo incremento seguido do teste. Um label é um identificador seguido por dois pontos. Em Java um label é usado quando se tem ciclos encaixados e se pretende que o efeito das instruções break ou continue se propaguem através de mais que um nível. Em Java um label coloca-se imediatamente antes de uma instrução de iteração ou switch. Um continue pára a execução da iteração corrente e regressa ao princípio do ciclo para começar uma nova iteração. Um continue com label salta para o label e entra no ciclo imediatamente a seguir ao label. Um break abandona o ciclo sem executar o resto das instruções do ciclo. Um break com label abandona não só o ciclo corrente mas todos os ciclos até ao denotado pelo label, o qual também abandona. Quando um break de um label causa a saída de um método pode-se usar simplesmente return. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 83 Arrays As semelhanças entre Java e C relativamente a arrays são apenas superficiais e baseadas apenas na sintaxe. Os arrays em C estão muito relacionados com apontadores. Java não revela os apontadores aos programadores. Não existem variáveis do tipo apontador. Java trata os apontadores implicitamente. Arrays são um tipo referência. Arrays são objectos. Quando se declara um array obtém-se uma variável que pode conter uma referência a um array. Mas ainda é necessário criar o array. Ao declararmos um array (assim como outro qualquer objecto) apenas obtemos uma localização de memória que pode conter um apontador para o objecto.Quando se instancia o objecto preenche-se essa localização. Declaração de array: ou int numeros []; int [] numeros; Criação do array: numeros = new int [20]; Declaração e criação de um array: ou int numeros [] = new int [20]; int [] numeros = new int [20]; Em Java todos estes dados são automaticamente inicializados a: 0 para inteiros; 0.0 para reais; ‘\0’ para caracteres; false para booleanos; null para variáveis referência. Depois de um array ser criado com um dado tamanho, não é possível modificar esse tamanho. O tamanho de um array é um campo de dados da classe Array. Em Java os arrays são alocados dinamicamente (em tempo de execução) enquanto que em C têm o tamanho fixo em tempo de compilação. Para alterar o tamanho de um array em Java poder-se-ia criar um outro array com o tamanho desejado e do mesmo tipo, copiar o conteúdo do array para o novo array criado (“System.arraycopy( ... );” e finalmente copiar a variável referência do novo array para a variável referência do antigo array. DEI - ISEP Fernando Mouta 84 Programação Orientada por Objectos Um array também pode ser criado por inicialização directa dos elementos do array: int valores [] = {1, 2, 3, 4, 5, 6}; Os literais incluem-se dentro de chavetas. Uma série de valores dentro de chavetas só podem ser usados em instruções de inicialização e não em instruções de atribuição posteriores. Os índices dos arrays são verificados em tempo de execução (“runtime”). Se um índice tenta uma referência fora dos limites do array, causa uma excepção e termina a execução. Os elementos de um array são acedidos do modo usual: valores[0] = 1; O tamanho de um array obtem-se referenciando <nomeDoArray>.length int a[] = new int [20]; a.length e não a.length() porque “length” é um “final data field” criado e instanciado quando o objecto array é criado. Não é uma chamada a um método. Arrays de arrays: Não há arrays multidimensionais em Java. Declaração de arrays de arrays: int matriz [] []; matriz = new int [2] [3]; ou ou int matriz [] [] = new int [2] [3]; int [] [] matriz = new int [2] [3]; Também se podem criar arrays de arrays por inicialização directa: int matriz [] [] = { {4, 6, 1}, {8, 1, 2}} Assim matriz[0][0] = 4 matriz[0][1] = 6 matriz[0][2] = 1 matriz[1][0] = 8 matriz[1][1] = 1 matriz[1][2] = 2 Em arrays de arrays, os arrays de nível mais baixo não necessitam de ter todos o mesmo tamanho. Ex. Fernando Mouta int b [] [] = { {1, 2}, {3, 4, 5}} DEI - ISEP Programação Orientada por Objectos Assim b[0][0] = 1 b[0][1] = 2 85 b[1][0] = 3 b[1][1] = 4 b[1][2] = 5 b[0] tem 2 elementos e b[1] tem 3 elementos. Passagem de arrays como parâmetros na chamada de métodos Os arrays, porque são objectos em Java, são passados a métodos através da variável referência. O nome de um array é uma referência para o objecto que contém os elementos do array e para a variável instância “length” que indica o número de elementos do array. Para passar um argumento array a um método, especifica-se o nome do array sem parênteses rectos. Não é necessário passar o tamanho do array, porque em Java todo o objecto Array conhece o seu próprio tamanho (atavés da variável instância “length”). Exemplo: Escreva um programa que calcule o triângulo de Pascal até uma profundidade de 10, armazenando cada fila do triângulo num array de tamanho apropriado e colocando cada array correspondente a uma fila num array de 10 arrays de inteiros. Um triângulo de Pascal é o seguinte padrão de números inteiros 1 1 1 1 1 1 1 1 2 3 4 5 6 7 1 3 6 10 15 21 1 4 10 20 35 1 1 5 15 35 1 6 21 1 7 1 no qual cada inteiro dentro do triângulo é a soma dos dois inteiros acima dele. DEI - ISEP Fernando Mouta 86 Programação Orientada por Objectos public class TrianguloPascal { private int triangulo[][]; /** Cria um triângulo de Pascal até uma profundidade especificada. */ public TrianguloPascal(int linhas) { triangulo = new int[linhas][]; for (int lin = 0; lin < linhas; lin++) { triangulo[lin] = new int[lin + 1]; if (lin == 0) triangulo[0][0] = 1; else for (int col = 0; col <= lin; col++) { triangulo[lin][col] = 0; // se não está na extremidade direita, adiciona o nodo de cima à direita if (col < lin) triangulo[lin][col] += triangulo[lin - 1][col]; // se não está na extremidade esquerda, adiciona o nodo de cima à esquerda if (col > 0) triangulo[lin][col] += triangulo[lin - 1][col - 1]; } } } /** Imprime o triangulo de Pascal */ public void print() { for (int i = 0; i < triangulo.length; i++) { for (int j = 0; j < triangulo[i].length; j++) System.out.print(triangulo[i][j] + " "); System.out.println(); } } /** Cria um triângulo de Pascal até uma profundidade de 10 e imprime-o. */ public static void main(String[] args) throws java.io.IOException { TrianguloPascal tp = new TrianguloPascal(10); tp.print(); System.in.read(); } } Fernando Mouta DEI - ISEP