Programação Orientada por Objectos (POO) Semestre de Verão de 2009-2010 1º Trabalho prático Data de Entrega: 19 de Abril de 2010 OBJECTIVOS: Exprimir objectivos na forma de algoritmos recursivos. Completar e desenvolver aplicações simples usando o paradigma da Programação Orientada por Objectos. NOTA: No relatório têm que constar todos os programas de teste que lhe permitiram validar a correcção dos métodos e classes realizadas. 1º Grupo Complete as seguintes frases: a) Algoritmos ____________ usam ciclos e se necessário estruturas de dados adicionais. Algoritmos ____________ invocam-se a si próprios até que certa condição seja satisfeita. b) Para verificar se um elemento existe num array ordenado deve-se usar o algoritmo de pesquisa ____________. Responda de forma sucinta às seguintes questões: a) Num algoritmo recursivo quando a chamada recursiva aparece no final, dá-se o nome de recursividade terminal ou final. Neste caso a última chamada recursiva pode ser transformada num ciclo. Como? b) Existe alguma relação entre stack overflow e recursão infinita? c) Quais os cuidados a ter nos algoritmos recursivos para que a recursão seja finita. Análise e implementação: 1. Indique, justificando, o resultado da execução de main e dê nomes sugestivos ao método e aos parâmetros. public static void xpto( String s, int x, int y ) { if ( x >= y ) xpto(s, x / y, y); System.out.print( s.charAt( x % y) ); } public static void main( String[] args) { xpto("01", 5, 2)); } 2. Realize um método com a seguinte assinatura, utilizando um algoritmo recursivo: String getCapicua( int n1, int n2 ), que recebe dois valores inteiros compreendidos entre 1 e 9, e retorne uma string com os valores de n1 a n2, seguidos dos valores de (n2-1) a n1. Exemplos de utilização: System.out.println( getCapicua( 1, 1 ) ); System.out.println( getCapicua( 1, 2 ) ); System.out.println( getCapicua( 3, 9 ) ); 1 121 3456789876543 3. Realize um método com a seguinte assinatura, utilizando um algoritmo recursivo: public static int countBits1( int n ) que recebe um inteiro positivo n e retorna o número de bits a 1 da representação em binário do valor inteiro positivo n. Exemplos de utilização: 31 System.out.println( countBits1( 0x7FFFFFFF ) ); 2 System.out.println( countBits1( 011 ) ); 4. Realize um método com a seguinte assinatura, utilizando um algoritmo recursivo: int[] findSum( int[] sortedArray, int sum ) que, dado o array ordenado de forma estritamente crescente sortedArray e o inteiro sum, retorna dois elementos desse array tal que a soma seja igual a sum. Esses dois elementos são retornados através de um array com duas posições. Caso não existam dois elementos cuja soma seja sum, deve ser retornado null. 2º Grupo 1. Pretende-se implementar uma solução para representar sequências de valores inteiros. Para o efeito chegou-se ao seguinte diagrama de classes: public class Sequence { private int currentIndex = 0; public final int getCurrentIndex() { return currentIndex; } public void advance() { if (hasElement()) ++ currentIndex;} public boolean hasElement() { return currentIndex < getSize(); } public int currentElement() { return 0; } public int getSize() { return 0; } } public class FixedSize extends Sequence { private final int size; public FixedSize( int sz ) { size = sz; } public final int getSize() { return size; } } public class InfiniteSequence extends Sequence { public final boolean hasElement() { return true; } public final int getSize() { return -1; } } A classe Sequence contém as operações comuns a todas as sequências. Uma sequência caracteriza-se pelos valores que a compõem, obtidos usando os métodos currentElement (que devolve o elemento actual), hasElement (verifica se foi atingido o final da sequência) e advance (avança para o próximo elemento, caso exista). O método getCurrentIndex devolve o número de ordem do elemento actual da sequência. O método getSize devolve o número de inteiros que compõem a sequência, caso seja finita, caso contrário retorna -1. A classe FixedSize é a classe base de todas as instâncias que representam sequências de n inteiros. A classe InfiniteSequence é a classe base de todas as instâncias que representam sequências infinitas. Tendo em consideração o diagrama de classes e a definição das classes Sequence, FixedSize e InfiniteSequence: Defina a classe EvenNumbers cujas instâncias representam sequências de n inteiros pares consecutivos a partir de um dado valor. No construtor são passados o número de inteiros da sequência e o limite inferior. Como exemplo considere o seguinte troço de código que produz o output: “0:2 – 1:4 – 2:6 – 3:8 –”. for ( Sequence s = new EvenNumbers(4, 1) ; s.hasElement() ; s.advance() ) if ( s.currentElement() != s.currentElement() ) System.out.print( "ERRO DE IMPLEMENTAÇÃO" ); else System.out.print( s.getCurrentIndex() + ":" + s.currentElement() + " – " ); Defina a classe SortArray cujas instâncias representam sequências de n inteiros ordenados de forma estritamente crescente contidos na parte do array definida pelos índices de bI (inclusivo) até eI (exclusivo). Como exemplo considere o seguinte troço de código que produz: “ 0:1 – 1:2 – 3:5 – 4:8 – 0:2 – 1:5 –”. int[] a = {7, 5, 8, 5, 2, 1 }; Sequence s1 = new SortArray(a, 1, 6), s2 = new SortArray(a, 3, 5); for (; s1.hasElement(); s1.advance() ) System.out.print( s1.getCurrentIndex() + ":" + s1.currentElement() + " – " ); for (; s2.hasElement() ; s2.advance() ) System.out.print( s2.getCurrentIndex() + ":" + s2.currentElement() + " – " ); Defina a classe RandomNumbers que representa uma sequência infinita pseudo-aleatória de números. A classe java.util.Random disponibiliza o método de instância nextInt que, sempre que é evocado, gera aleatoriamente um valor inteiro. O seguinte troço de código deve produzir 10 números aleatórios. for ( Sequence s = new RandomNumbers(); s.getCurrentIndex() < 10; s.advance() ) { if ( s.currentElement() != s.currentElement() ) System.out.print( "ERRO DE IMPLEMENTAÇÃO" ); else System.out.print( s.currentElement() + " " ); } Defina a classe Composite que representa sequências compostas pela concatenação de sequências. Acrescente à classe Composite o método add e redefina os métodos herdados de forma a que o troço de código abaixo apresentado produza o output presente na caixa de texto: Composite x = new Composite(); x.add( new SortArray( new int[] {4,2}, 0, 2) ); if ( ! x.add( new RandomNumbers() ) ) System.out.println("1 - Não é possível adicionar sequências infinitas"); Composite y = new Composite(); y.add( x ); x.add( new EvenNumbers(3, 12) ); if ( ! x.add( new Sequence() ) ) System.out.println("2 - Não é possível adicionar sequências vazias"); System.out.print( "Lista " + y.getSize() + ": " ); for ( ; y.hasElement() ; y.advance() ) System.out.print( y.getCurrentIndex() + ":" + y.currentElement() + " – " ); 1 - Não é possível adicionar sequências infinitas 2 - Não é possível adicionar sequências vazias Lista 5: 0:2 – 1:4 – 2:12 – 3:14 – 4:16 – 2. Observe o diagrama UML presente na seguinte Figura e considere-o nas alíneas seguintes Complete as seguintes frases: a) Através do diagrama anterior, verifica-se que a classe E _________________ a classe B e _________________ a interface I2. b) Podemos afirmar que todos os objectos do tipo B são do tipo ___ mas que nem todos do tipo B são do tipo ___. c) O método público print3 pode ser invocado sobre variáveis do tipo ___ enquanto o método __________ pode ser invocado sobre qualquer dos tipos que constam no diagrama. d) A declaração do método __________ na classe C está incorrecta porque …. e) Embora não esteja declarado, a classe D tem que ter pelo menos um __________ porque …. f) O troço de código I2 i2 = new I2(); origina um erro de... porque …. g) O troço de código I1 i1 = new A( 1 ); origina um erro de... porque …. h) Assumindo que a classe D tem um construtor sem parâmetros, o troço de código C c = new D(); origina um erro de... porque …. i) O troço de código I2 i2 = new D(); i2.print3(); origina erro de... porque …. j) O troço de código A a = new C(); D d = (D)a; d.print3() origina um erro de... porque …. Responda de forma sucinta às seguintes questões: a) A interface I2 podia ser transformada numa classe abstracta? b) O construtor da classe A é herdado pelas classes C e D? E pode ser sobrecarregado ou sobreposto? Defina as classes correspondentes ao diagrama de classes em que o método: print1 escreve no console output o nome da classe; print2 invoca primeiro o print1 da classe base e a seguir o print1 da própria classe; print3 invoca primeiro o print1 da própria classe e a seguir o print1 da classe base. 3. Pretende-se desenvolver uma aplicação que implemente o “Jogo do Adivinha”. Neste jogo, o jogador pensa em algo (pensamento) e a aplicação, através de uma sequência de perguntas, de resposta sim/não (decisão), tenta adivinhar em que é que o utilizador estava a pensar. Em http://www.akinator.com/ está disponível uma versão online deste jogo, com maior complexidade do que aquela que é pedida neste trabalho. O jogo usa uma árvore de decisão para manter a informação relativa aos pensamentos (elipses na Figura 1) e decisões (rectângulos na Figura 1). A Figura apresenta um exemplo da árvore de decisão, através da qual é possível adivinhar quatro animais usando três questões de decisão. Por exemplo, sabe-se que uma Cobra não tem Patas, é um Réptil e não é uma Ave (elementos com fundo amarelo na figura 1). Ave ? root: SIM NÃO Águia Réptil ? SIM Leão Patas ? SIM Lagartixa NÃO NÃO Cobra Figura 1: Árvore de decisão a) Defina uma hierarquia de tipos que suporte a representação da Figura 1, e construa a árvore de decisão. b) Acrescente suporte para: i. Apresentar a árvore de decisão root.show(“Sim”, “Não”); Resultado (exemplo para a árvore de decisão da Figura 1): Ave Sim: Águia Não: Réptil Sim: Patas Sim: Lagartixa Não: Cobra Não: Leão ii. Obter o número de pensamentos int nt = root.getNumberOfThoughts(); iii. // nt = 4 Verificar da existência de determinado pensamento boolean exists = root.containsThougth(“Cobra”); iv. // exists = true Mostrar as decisões positivas (Sim) relativas a determinado pensamento cobra.showDecisionPath(); lagartixa.showDecisionPath(); Cobra -> Réptil Lagartixa -> Patas -> Réptil c) Implemente a funcionalidade de jogo, de forma a ser possível: i. Pedir ao utilizador para pensar em algo e, com uma sequência de perguntas de resposta Sim/Não, adivinhar em que é que ele pensou. ii. No caso de derrota por parte da aplicação (errar na resposta), perguntar em que é que o utilizador estava a pensar (pensamento) e de forma é que é diferente (decisão), e adicionar essa informação à árvore existente. System.out.println(“Pense em algo.”); Response res = root.guess(kbd); // descobre em que estou a pensar e usa // kbd, para fazer perguntas ao utilizador if (res.success()) { System.out.println(“Acertei :-)”); res.showDecisionPath(); } else { System.out.println(“Ups, estava quase!. Afinal não é ” + res.getThought()); System.out.println(“Em que pensaste? ”); Thought newTh = new Thought(kbd.nextLine()); System.out.println(“O que o diferencia?”); Decision newDecision = new Decision(kbd.nextLine(), newTh); res.replaceWithDecision(newDecision); newTh.showDecisionPath(); } Exemplo em que o utilizador pensou em ”Cobra”: (texto do utilizador em itálico, sublinhado e negrito) Pense em algo. Ave? N Réptil? S Patas? S Pensou em Cobra? S Acertei :-) Cobra -> Réptil Exemplo em que o utilizador pensou em ”Peixe”: Pense em algo. Ave? N Réptil? N Pensou em Leão? N Ups, estava quase!. Afinal não é Leão. Em que pensaste? Peixe O que o diferencia? Escamas Peixe -> Escamas A Figura 2 mostra as alterações à árvore de decisão após adição do pensamento “Peixe” e respectiva pergunta de decisão “Escamas”. Note que o novo pensamento é sempre colocado no lado “Sim” da decisão, e que o pensamento anterior é colocado do lado oposto. A nova decisão é colocada na posição do antigo pensamento (no caso, “Escamas” substituí “Leão”). Ave ? root: Ave ? root: SIM NÃO SIM Águia Águia Réptil ? SIM SIM Lagartixa NÃO Cobra Réptil ? NÃO SIM Leão Patas ? NÃO Patas ? SIM Lagartixa NÃO Escamas ? NÃO Cobra SIM Peixe NÃO Leão Figura 2: Alterações à árvore de decisão após adição do “Peixe” Bom trabalho