Versão para Impressão
Versão para Impressão
Linguagens e paradigmas de programação
As informações desse capítulo foram retiradas basicamente de [1] e [2].
Introdução
No passado escrevia-se programas utilizando apenas linguagens de baixo nível. A escrita é engessada, complexa e muito específica, sendo pouco acessível para os desenvolvedores no geral. Esse tipo de linguagem exige muito conhecimento de quem a programa (inclusive relacionado à forma com que o processador opera uma instrução-máquina).
Recentemente foi liberado o código-conte utilizado no computador que guiou a missão Apollo que teve como principal objetivo levar o homem à lua (na tão famigerada corrida espacial entre a União Soviética e os EUA), o Apollo Guidance Computer.
Um programa escrito em uma dessas linguagens, chamadas de baixo nível, é composto por uma série de instruções de máquina que determinam quais operações o processador deve executar. Essas instruções são convertidas para a linguagem que o processador entende, que é a linguagem binária (sequência de bits 0 e 1), que é categorizada como First-generation programming language (1GL), em livre tradução: linguagem de programação de primeira geração.
Linguagens de alto nível
Com a popularidade dos computadores criou-se um "problema" de alta demanda por software e, consequentemente, por programadores. Talvez você esteja pensando que isso não é exatamente um problema, e sim uma coisa boa, uma tendência, um novo mercado. Faz sentido, até certo ponto. O problema era encontrar mão de obra qualificada para codificar àquelas instruções tão complicadas.
Com isso, novas linguagens surgiram e, cada vez mais, aproximavam-se da linguagem humana. Isso abriu "fronteiras" para que uma enorme gama de novos desenvolvedores se especializassem. Tais linguagens são denominadas como sendo de alto nível. As linguagens modernas que hoje conhecemos e usamos são de alto nível: C, PHP, Java, Rust, C#, Python, Ruby etc.
Dica
Quanto mais próxima da linguagem da máquina, mais baixo nível é a linguagem. Quanto mais próxima da linguagem humana, mais alto nível ela é.
Paradigmas das linguagens de programação
Quando uma linguagem de programação é criada, a partir das suas características, ela é categorizada em um ou mais paradigmas.
A definição do dicionário Aurélio para "paradigma":
- Algo que serve de exemplo geral ou de modelo.
- Conjunto das formas que servem de modelo de derivação ou de flexão.
- Conjunto dos termos ou elementos que podem ocorrer na mesma posição ou contexto de uma estrutura.
O paradigma de uma linguagem de programação é a sua identidade. Corresponde a um conjunto de características que, juntas, definem como ela opera e resolve os problemas. Algumas linguagens, inclusive, possuem mais de um paradigma, são as chamadas multi paradigmas.
Alguns dos principais paradigmas utilizados hoje no mercado:
- Funcional
- Lógico
- Declarativo
- Imperativo
- Orientado a objetos
- Orientado a eventos
Paradigma funcional
O foco desse paradigma está na avaliação de funções. Como na matemática quando temos, por exemplo, uma função
Se o valor de entrada for 2, o resultado da avaliação da nossa função será 4.
Algumas das linguagens que atendem a esse paradigma: F# (da Microsoft), Lisp, Heskell, Erlang, Elixir, Mathematica.
É possível desenvolver de forma "funcional" mesmo em linguagens não estritamente funcionais. Por exemplo, no PHP, que é uma linguagem multi paradigma, teríamos:
<?php
$sum = function($value) {
return $value + 2;
};
echo $sum(2); // 4Paradigma lógico
Também é conhecido como "restritivo". Muito utilizado em aplicações de inteligência artificial. Esse paradigma chega no resultado esperado a partir de avaliações lógico-matemáticas. Se você já estudou lógica de predicados ficará confortável em entender como uma linguagem nesse paradigma opera.
Principais elementos desse paradigma:
- Proposições: base de fatos concretos e conhecidos.
- Regras de inferência: definem como deduzir proposições.
- Busca: estratégias para controle das inferências.
Exemplo:
Proposição
Chico é um gato.
Regra de inferência
Todo gato é um felino.
Busca
Chico é um felino?
A resposta para a Busca acima precisa ser verdadeira. A conclusão lógica é:
Tips
Se Chico é um gato e todo gato é felino, então Chico é um felino.
A ideia básica da programação em lógica é:
Prof. Dr. Sílvio do Lago Pereira – DTI / FATEC-SP.
Oferecer um arcabouço que permita inferir conclusões desejadas, a partir de premissas, representando o conhecimento disponível, de uma forma que seja computacionalmente viável.
A linguagem mais conhecida que utiliza esse paradigma é a Prolog. Esse paradigma é pouco utilizado em aplicações comerciais, seu uso se dá mais na área acadêmica.
Paradigma declarativo
O paradigma declarativo é baseado no lógico e funcional. Linguagens declarativas descrevem o que fazem e não exatamente como suas instruções funcionam.
Linguagens de marcação são o melhor exemplo: HTML, XML, XSLT, XAML etc. Não obstante, o próprio Prolog – reconhecido primariamente pelo paradigma lógico – também é uma linguagem declarativa. Abaixo alguns exemplos dessas linguagens.
HTML:
<article>
<header>
<h1>Linguagens e paradigmas de programação</h1>
</header>
</article>SQL:
SELECT nome FROM usuario WHERE id = 10Paradigma imperativo
Você já ouviu falar em "programação procedural" ou em "programação modular"? De modo geral, são imperativas.
Linguagens clássicas como C, C++, PHP, Perl, C#, Ruby etc, "suportam" esse paradigma. Ele é focado na mudança de estados de variáveis (ao contrário dos anteriores).
Exemplo:
if(option == 'A') {
print("Opção 'A' selecionada.");
}A impressão só será realizada se o valor da variável
Paradigma orientado a objetos
Esse é, entre todos, talvez o mais difundido. Nesse paradigma, ao invés de construirmos nossos sistemas com um conjunto estrito de procedimentos, assim como se faz em linguagens "fortemente" imperativas como o Cobol, Pascal etc, na orientação a objetos utilizamos uma lógica bem próxima do mundo real, lidando com objetos, estruturas que já conhecemos e sobre as quais possuímos uma grande compreensão.
Dica
OO é sigla para orientação a objetos
O paradigma orientado a objetos tem uma grande preocupação em esconder o que não é importante e em realçar o que é importante. Nele, implementa-se um conjunto de classes que definem objetos. Cada classe determina o comportamento (definido nos métodos) e estados possíveis (atributos) de seus objetos, assim como o relacionamento entre eles.
Esse é o paradigma mais utilizado em aplicações comerciais e as principais linguagens o implementam: C#, Java, PHP, Ruby, C++, Python etc.
Paradigma orientado a eventos
Toda linguagem que faz uso de interface gráfica é baseada nesse paradigma. Nele, o fluxo de execução do software é baseado na ocorrência de eventos externos, normalmente disparados pelo usuário.
O usuário, ao interagir, decidirá em qual momento digitar, clicar no botão de "salvar" etc. Essas decisões dispararão eventos. O usuário é, então, o responsável por quando os eventos acontecerão, de tal forma que fluxo do programa fica sensivelmente atrelado à ocorrências desses eventos.
Linguagens de programação que fazem uso desse paradigma:
- Delphi
- Visual Basic
- C#
- Python
- Java etc.
Java
Nesse capítulo teremos um compilado de informações que foram reunidas de vários lugares ([3], [4], [5], [6]). Bons Estudos.
Por que Java?
Existem diversas linguagens de programação orientadas a objeto, cada uma com diferentes características e apelos de mercado, educacionais ou acadêmicos. Segue, algumas das razões da escolha da linguagem Java.
Java é obrigatoriamente orientada a objetos
: Algumas linguagens permitem que objetos e variáveis existam em diversos pontos de um programa, como se estivessem desatreladas de qualquer estrutura. Em Java, todas as variáveis e métodos devem estar localizados dentro de classes, forçando o uso de orientação a objetos até mesmo em tarefas simples. Dessa forma, o estudante de programação orientada a objetos que esteja usando Java estará usando mais as técnicas de POO.
Java é simples
: A estrutura de programas e classes em Java segue a organização de linguagens tradicionais como C e C++, mas sem elementos que tornam programas e programação mais complexos. Após o aprendizado dos conceitos básicos de programação orientada a objetos, o estudante da linguagem pode começar a criar aplicativos úteis e complexos. A simplicidade se reflete também na maneira com que arquivos contendo programas em Java são compilados e executados: se as recomendações básicas forem seguidas, o compilador se encarregará de compilar todas as classes necessárias em uma aplicação automaticamente, sem necessidade de arquivos adicionais de configuração e inclusão de bibliotecas.
Java é portátil
: O código-fonte de um programa ou classe em Java pode ser compilado em qualquer computador, usando qualquer sistema operacional, contanto que este tenha uma máquina virtual Java adequada. Adicionalmente, as classes criadas podem ser copiadas e executadas em qualquer computador nas mesmas condições, aumentando a utilidade da linguagem através da independência de plataformas, contanto que versões compatíveis da máquina virtual sejam usadas.
Java é gratuita
: A máquina virtual Java, mencionada acima, está à disposição para cópia no site da Oracle e em vários outros. Compiladores simples, de linha de comando (sem interfaces visuais elaboradas) fazem parte do JDK, o ambiente de desenvolvimento gratuito de Java. Aplicações em Java precisam de uma máquina virtual para sua execução, mas não existem limitações na distribuição da máquina virtual, fazendo de Java uma plataforma extremamente econômica para desenvolvedores e usuários finais.
Java é robusta
: Administração de memória (alocação e liberação) e o uso de ponteiros, duas das fontes de erros e bugs mais frequentes em programas em C e C++, são administrados internamente na linguagem, de forma transparente para o programador. De maneira geral, programas em Java tem restrições no acesso à memória que resultam em maior segurança para os programas sem diminuir a utilidade dos mesmos. Java também tem um poderoso mecanismo de exceções que permite melhor tratamento de erros em tempo de execução dos programas.
Java tem bibliotecas prontas para diversas aplicações
: As bibliotecas de classes de Java contém várias classes que implementam diversos mecanismos de entrada e saída, acesso à Internet, manipulação de Strings em alto nível, poderosas estruturas de dados, utilitários diversos e um conjunto completo de classes para implementação de interfaces gráficas. Vale a pena relembrar que estas bibliotecas são padrão de Java - qualquer máquina virtual Java permite o uso destas bibliotecas, sem a necessidade de instalar pacotes adicionais, e que mesmo que o compilador usado não tenha interface gráfica similar à de linguagens visuais, os programas criados com este compilador podem ter interfaces gráficas complexas.
História
A história da tecnologia Java começou modestamente no final de 1990, quando a empresa Sun Microsystems encarregou seus funcionários Patrick Naughton, Mike Sheridan e James Gosling da tarefa de descobrir qual seria a próxima grande tendência na área da computação. O projeto, denominado "Green Project", chegou à seguinte conclusão preliminar: a integração dos dispositivos controlados digitalmente com os computadores iria se tornar uma tendência muito importante.
Aquele revolucionário dispositivo remoto, conectado à rede sem fio, rodava uma versão do sistema operacional Unix e possuía uma tela sensível ao toque (touchscreen) – lembre-se que estávamos em 1992 e, para se ter uma ideia do desenvolvimento da Internet naquela época, basta dizer que, no final do ano, havia apenas 26 servidores Web no mundo! A interface de usuário incluía também um assistente de ajuda: um simpático personagem de desenho animado chamado Duke, que mais tarde seria adotado como mascote da plataforma Java.
O Star 7 incorporava "uma nova linguagem de programação dinâmica, compacta, segura, distribuída, robusta, interpretada, incorporando garbage collection e multi-threading, neutra à arquitetura de hardware e de alto desempenho" a fim de solucionar os vários problemas pertinentes ao desenvolvimento de programas para execução na plataforma Star 7. James Gosling criou a linguagem e deu-lhe o nome de "Oak", em homenagem a um enorme carvalho que podia ser visto da janela do seu escritório.
O "Green Project" teve o seu grande momento de glória e surgiu a oportunidade de produzir dispositivos similares ao Star 7 para consumidores em potencial na indústria de TV a cabo. "Equipe Green" mudou de nome para "FirstPerson" e preparou um filme para demonstração da sua tecnologia aos produtores de transcodificadores de TV e aluguel de vídeo. Mas, infelizmente para a equipe, aqueles setores, ainda na sua infância, estavam em processo de criação de modelos de negócio viáveis. A despeito do insucesso da "FirstPerson" com a TV a cabo, o tipo de configuração da sua tecnologia de rede era idêntica à configuração de rede para a Internet, que passava por um avassalador processo de popularização.
A Internet usava a HyperText Markup Language (HTML) para transportar o conteúdo de mídia – texto, vídeo, imagens, áudio, etc. – através de uma rede de dispositivos heterogêneos. De uma forma similar, Java (um nome substituto para Oak, a fim de contornar problemas de natureza legal com outra linguagem de nome idêntico) também movimentava o conteúdo de mídia através de uma rede de dispositivos heterogêneos, mas ia além: transportava também o seu "comportamento", na forma de applets. A linguagem HTML não conseguia fazer isso.
Para demonstrar a utilidade de Java numa Web baseada na Internet, a "FirstPerson" fez um clone do navegador Mosaic e, em 1994, o navegador WebRunner (mais tarde conhecido como HotJava), "deu vida, pela primeira vez, a objetos animados e a conteúdo executável dinamicamente dentro de um navegador."
John Gage, diretor do Escritório de Ciências da Sun, e James Gosling, fizeram uma demonstração do WebRunner numa reunião com profissionais da Internet e da área de entretenimento no início de 1995. A plateia aplaudiu calorosamente as demonstrações de uma molécula tridimensional giratória e a animação de um algorítimo de ordenação preparado por Gosling, embutidas em páginas Web apresentadas no WebRunner. O futuro certamente havia chegado. A notícia se espalhou rapidamente, e a "FirstPerson" disponibilizou o WebRunner para download.
Milhares de downloads chamaram a atenção da Sun para a popularidade de Java, e ela disponibilizou uma área no seu site para divulgar essa tecnologia. Em maio de 1995, durante a abertura da feira SunWorld da Sun, o líder do projeto Netscape, Marc Andreessen, anunciou que iriam integrar Java ao novo navegador para a Web Netscape.
O Java é agora utilizado para desenvolver aplicativos corporativos de grande porte, aprimorar a funcionalidade de servidores da web (os computadores que fornecem o conteúdo que vemos em nossos navegadores), fornecer aplicativos para dispositivos voltados ao consumo popular (por exemplo, telefones celulares, smartphones, televisão, set-up boxes etc.) e para muitos outros propósitos. Ainda, ele também é a linguagem-chave para desenvolvimento de aplicativos Android adequados a smartphones e tablets.
Devida a essa grande aplicabilidade em abril de 2009, a Oracle ofereceu US$ 7,4 bilhões pela aquisição da Sun Microsystems e a proposta foi aceita. Essa aquisição deu à Oracle a propriedade de vários produtos, incluindo o Java e o sistema operacional Solaris. Em comunicado, a Oracle afirmou que o Java foi o software mais importante adquirido ao longo de sua história. Muitas especulações foram feitas a cerca do futuro do Java depois de passar a ser propriedade da Oracle. Mais com certeza essa aquisição contribuiu muito para que o Java tivesse um salto qualitativo.
Máquina Virtual
Em uma linguagem de programação como C e Pascal, temos a seguinte situação quando vamos compilar um programa:
O código fonte é compilado para código de máquina específico de uma plataforma e sistema operacional.
Muitas vezes o próprio código fonte é desenvolvido visando uma única plataforma!
Esse código executável (binário) resultante será executado pelo sistema operacional e, por esse motivo, ele deve saber conversar com o sistema operacional em questão.
Isto é, temos um código executável para cada sistema operacional. É necessário compilar uma vez para Windows, outra para o Linux, e assim por diante, caso a gente queira que esse nosso software possa ser utilizado em várias plataformas. Esse é o caso de aplicativos como o Eclipse, Firefox e outros.
Como foi dito anteriormente, na maioria das vezes, a sua aplicação se utiliza das bibliotecas do sistema operacional, como, por exemplo, a de interface gráfica para desenhar as "telas". A biblioteca de interface gráfica do Windows é bem diferente das do Linux: como criar então uma aplicação que rode de forma parecida nos dois sistemas operacionais?
Precisamos reescrever um mesmo pedaço da aplicação para diferentes sistemas operacionais, já que eles não são compatíveis.
Já o Java utiliza do conceito de máquina virtual, onde existe, entre o sistema operacional e a aplicação, uma camada extra responsável por "traduzir" - mas não apenas isso - o que sua aplicação deseja fazer para as respectivas chamadas do sistema operacional onde ela está rodando no momento:
Dessa forma, a maneira com a qual você abre uma janela no Linux ou no Windows é a mesma: você ganha independência de sistema operacional. Ou, melhor ainda, independência de plataforma em geral: não é preciso se preocupar em qual sistema operacional sua aplicação está rodando, nem em que tipo de máquina, configurações, etc.
Perceba que uma máquina virtual é um conceito bem mais amplo que o de um interpretador. Como o próprio nome diz, uma máquina virtual é como um "computador de mentira": tem tudo que um computador tem. Em outras palavras, ela é responsável por gerenciar memória, threads, a pilha de execução, etc.
Sua aplicação roda sem nenhum envolvimento com o sistema operacional! Sempre conversando apenas com a Java Virtual Machine (JVM).
Essa característica é interessante: como tudo passa pela JVM, ela pode tirar métricas, decidir onde é melhor alocar a memória, entre outros. Uma JVM isola totalmente a aplicação do sistema operacional. Se uma JVM termina abruptamente, só as aplicações que estavam rodando nela irão terminar: isso não afetará outras JVMs que estejam rodando no mesmo computador, nem afetará o sistema operacional.
Essa camada de isolamento também é interessante quando pensamos em um servidor que não pode se sujeitar a rodar código que possa interferir na boa execução de outras aplicações.
Essa camada, a máquina virtual, não entende código java, ela entende um código de máquina específico. Esse código de máquina é gerado por um compilador java, como o javac, e é conhecido por bytecode, pois existem menos de 256 códigos de operação dessa linguagem, e cada "opcode" gasta um byte. O compilador Java gera esse bytecode que, diferente das linguagens sem máquina virtual, vai servir para diferentes sistemas operacionais, já que ele vai ser "traduzido" pela JVM.
Write once, run anywhere
Esse era um slogan que a Sun usava para o Java, já que você não precisa reescrever partes da sua aplicação toda vez que quiser mudar de sistema operacional
Java lento? Hotspot e JIT
Hotspot é a tecnologia que a JVM utiliza para detectar pontos quentes da sua aplicação: código que é executado muito, provavelmente dentro de um ou mais loops. Quando a JVM julgar necessário, ela vai compilar estes códigos para instruções realmente nativas da plataforma, tendo em vista que isso vai provavelmente melhorar a performance da sua aplicação. Esse compilador é o JIT: Just inTime Compiler, o compilador que aparece "bem na hora" que você precisa.
Você pode pensar então: porque a JVM não compila tudo antes de executar a aplicação? É que teoricamente compilar dinamicamente, a medida do necessário, pode gerar uma performance melhor. O motivo é simples: imagine um .exe gerado pelo VisualBasic, pelo gcc ou pelo Delphi; ele é estático. Ele já foi otimizado baseado em heurísticas, o compilador pode ter tomado uma decisão não tão boa.
Já a JVM, por estar compilando dinamicamente durante a execução, pode perceber que um determinado código não está com performance adequada e otimizar mais um pouco aquele trecho, ou ainda mudar a estratégia de otimização. É por esse motivo que as JVMs mais recentes em alguns casos chegam a ganhar de códigos C compilados com o GCC 3.x.
Versões do Java e a confusão do Java2
Java 1.0 e 1.1 são as versões muito antigas do Java, mas já traziam bibliotecas importantes como o JDBC e o java.io.
Com o Java 1.2 houve um aumento grande no tamanho da API, e foi nesse momento em que trocaram a nomenclatura de Java para Java2, com o objetivo de diminuir a confusão que havia entre Java e Javascript. Mas lembre-se, não há versão "Java 2.0", o 2 foi incorporado ao nome, tornando-se Java2 1.2.
Depois vieram o Java2 1.3 e 1.4, e o Java 1.5 passou a se chamar Java 5, tanto por uma questão de marketing e porque mudanças significativas na linguagem foram incluídas. É nesse momento que o "2" do nome Java desaparece. Perceba que para fins de desenvolvimento, o Java 5 ainda é referido como Java 1.5. Hoje a última versão disponível do Java é a 20 [7].
JVM? JRE? JDK? O que devo baixar?
O que gostaríamos de baixar no site da Oracle?
JVM
: Apenas a virtual machine, esse download não existe, ela sempre vem acompanhada.
JRE
: Java Runtime Environment, ambiente de execução Java, formado pela JVM e bibliotecas, tudo que você precisa para executar uma aplicação Java. Mas nós precisamos de mais.
JDK
: Java Development Kit: Nós, desenvolvedores, faremos o download do JDK do Java SE (Standard Edition). Ele é formado pela JRE somado a ferramentas, como o compilador.
Tanto o JRE e o JDK podem ser baixados do site http://www.oracle.com/technetwork/java/. Para encontrá-los, acesse o link Java SE dentro dos top downloads. Consulte o apêndice de instalação do JDK para maiores detalhes.
Referências
Codificando com JAVA
Em geral, as linguagens de programação possuem convenções [8] para definir os nomes das variáveis. Essas convenções ajudam o desenvolvimento de um código mais legível.
Na convenção de nomes da linguagem Java, os nomes das variáveis devem seguir o padrão camel case com a primeira letra minúscula (lower camel case). Veja alguns exemplos:
- nomeDoCliente
- numeroDeAprovados
Declarando e usando variáveis
Dentro de um bloco, podemos declarar variáveis e usá-las. Em Java, toda variável tem um tipo que não pode
ser mudado, uma vez que declarado:
tipoDaVariavel nomeDaVariavel;Por exemplo, é possível ter uma idade que guarda um número inteiro:
int idade;Com isso, você declara a variável idade, que passa a existir a partir daquela linha. Ela é do tipo int, que guarda um número inteiro. A partir daí, você pode usá-la, primeiramente atribuindo valores.
A linha a seguir é a tradução de: "idade deve valer quinze”.
idade = 15;Além de atribuir, você pode utilizar esse valor. O código a seguir declara novamente a variável idade com valor 15 e imprime seu valor na saída padrão através da chamada a System.out.println.
// declara a idade
int idade;
idade = 15;
// imprime a idade
System.out.println(idade);Por fim, podemos utilizar o valor de uma variável para algum outro propósito, como alterar ou definir uma segunda variável. O código a seguir cria uma variável chamada idadeNoAnoQueVem com valor de idade mais um.
// calcula a idade no ano seguinte
int idadeNoAnoQueVem;
idadeNoAnoQueVem = idade + 1;No mesmo momento que você declara uma variável, também é possível inicializá-la por praticidade:
int idade = 15;Você pode usar os operadores
int quatro = 2 + 2;
int tres = 5 - 2;
int oito = 4 * 2;
int dezesseis = 64 / 4;
int um = 5 % 2; // 5 dividido por 2 dá 2 e tem resto 1;
// o operador % pega o resto da divisão inteiraRepresentar números inteiros é fácil, mas como guardar valores reais, tais como frações de números inteiros e outros? Outro tipo de variável muito utilizado é o double, que armazena um número com ponto flutuante (e que também pode armazenar um número inteiro).
double pi = 3.14;
double x = 5 * 10;O tipo boolean armazena um valor verdadeiro ou falso, e só: nada de números, palavras ou endereços, como em algumas outras linguagens.
boolean verdade = true;true e false são palavras reservadas do Java. É comum que um boolean seja determinado através de uma expressão booleana, isto é, um trecho de código que retorna um booleano, como o exemplo:
int idade = 30;
boolean menorDeIdade = idade < 18;O tipo char guarda um, e apenas um, caractere. Esse caractere deve estar entre aspas simples. Não se esqueça dessas duas características de uma variável do tipo char! Por exemplo, ela não pode guardar um código como " pois o vazio não é um caractere!
char letra = 'a';
System.out.println(letra);Variáveis do tipo char são pouco usadas no dia a dia Veremos mais a frente o uso das Strings, que usamos constantemente, porém estas não são definidas por um tipo primitivo.
Tipos primitivos e valores
Esses tipos de variáveis são tipos primitivos do Java: o valor que elas guardam são o real conteúdo da variável. Quando você utilizar o operador de atribuição = o valor será copiado.
int i = 5; // i recebe uma cópia do valor 5
int j = i; // j recebe uma cópia do valor de i
i = i + 1; // i vira 6, j continua 5Aqui, i fica com o valor de 6. Mas e j? Na segunda linha, j está valendo 5. Quando i passa a valer 6, será
que j também muda de valor? Não, pois o valor de um tipo primitivo sempre é copiado.
Apesar da linha 2 fazer j = i, a partir desse momento essas variáveis não tem relação nenhuma: o que acontece com uma, não repete em nada com a outra
O if e o else
A sintaxe do if no Java é a seguinte:
if (condicaoBooleana) {
codigo;
}Uma condição booleana é qualquer expressão que retorne true ou false. Para isso, você pode usar os operadores <, >, <=, >= e outros. Um exemplo:
int idade = 15;
if (idade < 18) {
System.out.println("Não pode entrar");
}Além disso, você pode usar a cláusula else para indicar o comportamento que deve ser executado no caso da expressão booleana ser falsa:
int idade = 15;
if (idade < 18) {
System.out.println("Não pode entrar");
} else {
System.out.println("Pode entrar");
}Você pode concatenar expressões booleanas através dos operadores lógicos "E" e "OU". O "E" é representado pelo && e o "OU" é representado pelo ||.
Um exemplo seria verificar se ele tem menos de 18 anos e se ele não é amigo do dono:
int idade = 15;
boolean amigoDoDono = true;
if (idade < 18 && amigoDoDono == false) {
System.out.println("Não pode entrar");
}
else {
System.out.println("Pode entrar");
}Esse código poderia ficar ainda mais legível, utilizando-se o operador de negação, o !. Esse operador transforma o resultado de uma expressão booleana de false para true e vice versa.
int idade = 15;
boolean amigoDoDono = true;
if (idade < 18 && !amigoDoDono) {
System.out.println("Não pode entrar");
} else {
System.out.println("Pode entrar");
}Perceba na linha 3 que o trecho amigoDoDono == false virou !amigoDoDono. Eles têm o mesmo valor.
Para comparar se uma variável tem o mesmo valor que outra variável ou valor, utilizamos o operador ==. Perceba que utilizar o operador = dentro de um if vai retornar um erro de compilação, já que o operador = é o de atribuição.
int mes = 1;
if (mes == 1) {
System.out.println("Você deveria estar de férias");
}Loops
O While
O while é um comando usado para fazer um laço (loop), isto é, repetir um trecho de código algumas vezes. A ideia é que esse trecho de código seja repetido enquanto uma determinada condição permanecer verdadeira.
int idade = 15;
while (idade < 18) {
System.out.println(idade);
idade = idade + 1;
}O trecho dentro do bloco do while será executado até o momento em que a condição idade < 18 passe a ser falsa. E isso ocorrerá exatamente no momento em que idade == 18, o que não o fará imprimir 18.
int i = 0;
while (i < 10) {
System.out.println(i);
i = i + 1;
}Já o while acima imprime de 0 a 9.
O For
Outro comando de loop extremamente utilizado é o for. A ideia é a mesma do while: fazer um trecho de código ser repetido enquanto uma condição continuar verdadeira. Mas além disso, o for isola também um espaço para inicialização de variáveis e o modificador dessas variáveis. Isso faz com que fiquem mais legíveis, as variáveis que são relacionadas ao loop:
for (inicializacao; condicao; incremento) {
codigo;
}Um exemplo é o a seguir:
for (int i = 0; i < 10; i = i + 1) {
System.out.println("olá!");
}Perceba que esse for poderia ser trocado por:
int i = 0;
while (i < 10) {
System.out.println("olá!");
i = i + 1;
}Porém, o código do for indica claramente que a variável i serve, em especial, para controlar a quantidade de laços executados. Quando usar o for? Quando usar o while? Depende do gosto e da ocasião.
Controlando loops
Apesar de termos condições booleanas nos nossos laços, em algum momento, podemos decidir parar o loop por algum motivo especial sem que o resto do laço seja executado.
int x = 100;
int y = 200;
for (int i = x; i < y; i++) {
if (i % 19 == 0) {
System.out.println("Achei um número divisível por 19 entre x("+x+") e y("+y+")");
System.out.println(i);
break;
}
}O código acima vai percorrer os números de x a y e parar quando encontrar um número divisível por 19, uma vez que foi utilizada a palavra chave break.
Da mesma maneira, é possível obrigar o loop a executar o próximo laço. Para isso usamos a palavra chave continue.
for (int i = 0; i < 100; i++) {
if (i > 50 && i < 60) {
continue;
}
System.out.println(i);
}?
O código acima não vai imprimir alguns números. (Quais exatamente?)
Escopos e Blocos
Escopo das variáveis
No Java, podemos declarar variáveis a qualquer momento. Porém, dependendo de onde você as declarou, ela vai valer de um determinado ponto a outro.
// aqui a variável i não existe
int i = 5;
// a partir daqui ela existeO escopo da variável é o nome dado ao trecho de código em que aquela variável existe e onde é possível acessá-la.
Quando abrimos um novo bloco com as chaves, as variáveis declaradas ali dentro só valem até o fim daquele bloco.
// aqui a variável i não existe
int i = 5;
// a partir daqui ela existe
while (condicao) {
// o i ainda vale aqui
int j = 7;
// o j passa a existir
}
// aqui o j não existe mais, mas o i continua dentro do escopoNo bloco acima, a variável j para de existir quando termina o bloco onde ela foi declarada. Se você tentar acessar uma variável fora de seu escopo, ocorrerá um erro de compilação.
O mesmo vale para um if:
if (algumBooleano) {
int i = 5;
} else {
int i = 10;
}
System.out.println(i); // cuidado!Aqui a variável i não existe fora do if e do else! Se você declarar a variável antes do if, vai haver outro erro de compilação: dentro do if e do else a variável está sendo redeclarada! Então o código para compilar e fazer sentido fica:
int i;
if (algumBooleano) {
i = 5;
} else {
i = 10;
}
System.out.println(i);Uma situação parecida pode ocorrer com o for:
for (int i = 0; i < 10; i++) {
System.out.println("olá!");
}
System.out.println(i); // cuidado!Neste for, a variável i morre ao seu término, não podendo ser acessada de fora do for, gerando um erro de compilação. Se você realmente quer acessar o contador depois do loop terminar, precisa de algo como:
int i;
for (i = 0; i < 10; i++) {
System.out.println("olá!");
}
System.out.println(i);Um bloco dentro do outro
Um bloco também pode ser declarado dentro de outro. Isto é, um if dentro de um for, ou um for dentro de um for, algo como:
while (condicao) {
for (int i = 0; i < 10; i++) {
// código
}
}Array
O problema
Dentro de um bloco, podemos declarar diversas variáveis e usá-las:
int idade1;
int idade2;
int idade3;
int idade4;Isso pode se tornar um problema quando precisamos mudar a quantidade de variáveis a serem declaradas de acordo com um parâmetro. Esse parâmetro pode variar, como por exemplo, a quantidade de número contidos num bilhete de loteria. Um jogo simples possui 6 números, mas podemos comprar um bilhete mais caro, com 7 números ou mais.
Para facilitar esse tipo de caso podemos declarar um vetor (array) de inteiros:
int[] idades = new int[10];O que fazemos foi criar uma array de int de 10 posições e atribuir o endereço no qual ela foi criada. Podemos ainda acessar as posições do array:
idades[5] = 8;O código acima altera a sexta posição do array. No Java, os índices do array vão de 0 a n-1, onde n é o tamanho dado no momento em que você criou o array.
Caution
Se você tentar acessar uma posição fora desse alcance, um erro ocorrerá durante a execução.
- Em Java, os arrays são criados através do comando new.
int[] numeros = new int[100];A variável numeros armazena a referência de um array criado na memória do computador através do comando new. Na memória, o espaço ocupado por esse array está dividido em 100 "pedaços" iguais numerados de 0 até 99. Cada "pedaço" pode armazenar um valor do tipo int.
Modificando o conteúdo de um array
Para modificar o conteúdo de um array, devemos escolher uma ou mais posições que devem ser alteradas e utilizar a sintaxe abaixo:
int[] numeros = new int[100];
numeros[0] = 136;
numeros[99] = 17;Também podemos definir os valores de cada posição de um array no momento da sua criação utilizando as sintaxes abaixo:
int[] numeros = new int[]{100 ,87};
int[] numeros = {100 ,87};Acessando o conteúdo de um array
Para acessar o conteúdo de um array, devemos escolher uma ou mais posições e utilizar a sintaxe abaixo:
int[] numeros = {100 ,87};
System.out.println(numeros[0]);
System.out.println(numeros[1]);Percorrendo um Array
Quando trabalhamos com um array, uma das tarefas mais comuns é acessarmos todas ou algumas de suas posições sistematicamente. Geralmente, fazemos isso para resgatar todos ou alguns dos valores armazenados e realizar algum processamento sobre tais informações. Para percorrermos um array, utilizaremos a instrução de repetição for. Podemos utilizar a instrução while também. Porém, logo perceberemos que a sintaxe da instrução for, em geral, é mais apropriada quando estamos trabalhando com arrays.
int[] numeros = new int[100];
for(int i = 0; i < 100; i ++) {
numeros[i] = i ;
}Para percorrer um array, é necessário saber a quantidade de posições do mesmo. Essa quantidade é definida quando o array é criado através do comando new. Nem sempre essa informação está explícita no código. Por exemplo, considere um método que imprima na saída padrão os valores armazenados em um array. Provavelmente, esse método receberá como parâmetro um array e a quantidade de posições desse array não estará explícita no código fonte.
void imprimeArray (int[] numeros ) {
// implementação
}Podemos recuperar a quantidade de posições de um array acessando o seu atributo length.
void imprimeArray (int[] numeros ) {
for(int i = 0; i < numeros.length; i++) {
System.out.println(numeros[i]) ;
}
}foreach
Para acessar todos os elementos de um array, é possível aplicar o comando for com uma sintaxe um pouco diferente.
void imprimeArray (int[] numeros ) {
for(int numero : numeros ) {
System.out.println(numero) ;
}
}void imprimeArray (int[] numeros ) {
for(int i = 0; i < numeros.length; i++) {
int numero = numeros[i];
System.out.println(numero);
}
}Operações
Nas bibliotecas da plataforma Java, existem métodos que realizam algumas tarefas úteis relacionadas a arrays. Veremos esses métodos a seguir.
Ordenando um Array
Considere um array de String criado para armazenar nomes de pessoas. Podemos ordenar esses nomes através do método Arrays.sort().
String[] nomes = new String[]{"rafael cosentino", "jonas hirata", "marcelo martins"};
Arrays.sort(nomes);
for( String nome : nomes ) {
System.out.println(nome);
}Analogamente, também podemos ordenar números.
Duplicando um Array
Para copiar o conteúdo de um array para outro com maior capacidade, podemos utilizar o método Arrays.copyOf().
String[] nomes = new String[] {"rafael", "jonas", "marcelo"};
String[] nomesDuplicados = Arrays.copyOf( nomes , 10) ;Preenchendo um Array
Podemos preencher todas as posições de um array com um valor específico utilizando o método Arrays.fill().
int[] numeros = new int[10];
java.util.Arrays.fill(numeros,5) ;Entrada e Saida de Dados
Quando falamos em entrada e saída, estamos nos referindo a qualquer troca de informação entre uma aplicação e o seu exterior.
A leitura do que o usuário digita no teclado, o conteúdo obtido de um arquivo ou os dados recebidos pela rede são exemplos de entrada de dados. A impressão de mensagens no console, a escrita de texto em um arquivo ou envio de dados pela rede são exemplos de saída de dados.
A plataforma Java oferece diversas classes e interfaces para facilitar o processo de entrada e saída. Em determinadas situações, uma aplicação precisa fazer a entrada e saída byte a byte mas, nem sempre isso é necessário. Sendo assim, é mais simples utilizar a classe Scanner do pacote java.util do Java. Essa classe possui métodos mais sofisticados para obter os dados de uma entrada.
Veja um exemplo de leitura do teclado com a classe Scanner:
import java.util.Scanner;
public class TestaDeclaracaoScanner {
public static void main(String[] args) {
//Lê a partir da linha de comando
Scanner teclado = new Scanner(System.in);
//Lendo um valor inteiro:
int n;
System.out.printf("Informe um número para a tabuada: ");
n = teclado.nextInt();
//Lendo um valor real:
float preco;
System.out.printf("Informe o preço da mercadoria = R$ ");
preco = teclado.nextFloat();
// Lendo um valor real:
double salario;
System.out.printf("Informe o salário do Funcionário = R$ ");
salario = teclado.nextDouble();
// Lendo uma String, usado na leitura de palavras simples que não usam o caractere de espaço (ou barra de espaço):
String s;
System.out.printf("Informe uma palavra simples:\n");
s = teclado.next();
// Lendo uma String, usado na leitura de palavras compostas, por exemplo, Pato Branco:
String s;
System.out.printf("Informe uma cadeia de caracteres:\n");
s = teclado.nextLine();
// Na leitura consecutiva de valores numéricos e String deve-se esvaziar o buffer do teclado antes da leitura do valor String, por exemplo:
int n;
String s;
System.out.printf("Informe um Número Inteiro: ");
n = teclado.nextInt();
teclado.nextLine(); // esvazia o buffer do teclado
System.out.printf("Informe uma cadeia de caracteres:\n");
s = teclado.nextLine();
}
}JOptionPane
Até agora vimos o método
System.out.printlnpara escrever informações na tela (console).A linguagem Java oferece diversas formas de interação com o usuário, a grande maioria em janelas.
Para evitar a criação de uma interface completa, pode-se utilizar as chamadas caixas de diálogo.
JOptionPane Oferece caixas de diálogo predefinidas que permitem aos programas exibir mensagens aos usuários;
exibir uma caixa de mensagem para informar o usuário, usamos o método showMessageDialog(...):
import javax.swing.JOptionPane;
public class Main {
public static void main (String arg[]) {
JOptionPane.showMessageDialog(null, "Olá JOptionPane");
System.exit(0);
}
}- Há uma outra forma de chamada para o método showMessageDialog, a qual permite melhorarmos o visual da caixa de mensagem:
JOptionPane.showMessageDialog(null,"Esta é uma mensagem", "Atenção", JOptionPane.WARNING_MESSAGE);- JOptionPane.PLAIN_MESSAGE
- nenhum ícone
- JOptionPane.ERROR_MESSAGE
- ícone de erro
- JOptionPane.INFORMATION_MESSAGE
- ícone de informação
- JOptionPane.WARNING_MESSAGE
- ícone de aviso
- JOptionPane.QUESTION_MESSAGE
- ícone de interrogação
System.exit
//...
System.exit(0);
//...- System.exit(0) é necessário em programas com interface gráfica, terminando o aplicativo Java.
- O retorno Zero('0') para o método exit() indica que o programa finalizou com sucesso.
- Valores diferentes de zero significam erros na execução e podem ser tratados por aplicativos que chamaram o programa Java.
showInputDialog
- Exibir uma caixa de entrada
- Retorna sempre a String digitada pelo usuário.
String nome;
nome = JOptionPane.showInputDialog("Digite o seu nome");
JOptionPane.showMessageDialog(null,"Seu nome é "+nome);- Variação mais completa:
nome = JOptionPane.showInputDialog(null, "Por favor, digite o seu nome", "Atenção", JOptionPane.INFORMATION_MESSAGE);Conversões em Java
Convertendo ASCII para String
Você pode converter códigos ASCII para String utilizando o método toString(), de acordo com o código abaixo:
int i = 64;
String aChar = new Character((char)i).toString();Convertendo números em decimal para binário
É possível fazer a conversão de números na base hexadecimal para binário através do método toBinaryString(), como pode ser visto a seguir:
int i = 42;
String binstr = Integer.toBinaryString(i);Convertendo um Double para um String
Você pode converter um variável do tipo double para um String usando o método toString() da classe Double, como apresentado a seguir:
double i = 42.0;
String str = Double.toString(i);Convertendo um float para um String
Da mesma forma que a conversão do Double, você utiliza o método toString() da classe Float.
float f = 12.0f;
String str = Float.toString(f);Convertendo um integer para código ASCII
Veja como fazer a conversão de um integer para ASCII:
char c = 'A';
int i = (int) c; // Você terá o valor 65Convertendo de um integer para uma String
Veja no código abaixo duas formas de fazer a conversão de um integer para uma String:
int i = 42;
String str = Integer.toString(i);
//ou
String str = "" + i ;Convertendo de um long para uma String
Você pode fazer a conversão de long para String através do método toString da classe Long.
long l = 42;
String str = Long.toString(l);Convertendo de uma String para Double
Você pode converter um String para double utilizando o método valueOf() e doubleValue() da classe Double, como mostrado no trecho abaixo.
double d = Double.valueOf(str).doubleValue();Convertendo String para integer
Faça a conversão de um String para integer usando o método parseInt() da classe Integer, ou usando os métodos valueOf() e intValue() da classe Integer combinados, como mostra o código a seguir.
str = "25";
int i = Integer.valueOf(str).intValue();
//ou
int i = Integer.parseInt(str);Convertendo uma String para um float
Converta um String para float através da combinação dos métodos valueOf() e floatValue() da classe Float.
float f = Float.valueOf(str).floatValue();Convertendo uma String para um long
Você pode fazer a conversão de um String para long usando o método parseLong() da classe Long, ou utilizando a combinação dos métodos valueOf() e longValue() também da classe Long.
long l = Long.valueOf(str).longValue();
//ou
long l = Long.parseLong(str);Enum
Um Java Enum é um tipo especial do Java usado para definir coleções de constantes. Mais precisamente, um tipo de enum Java é um tipo especial de classe Java. Um enum pode conter constantes, métodos, etc. Enums Java foram adicionados no Java 5.
Exemplo Enum
Aqui está um exemplo simples de enum Java:
public enum Level {
HIGH,
MEDIUM,
LOW
}Observe a palavra enum é usada no lugar de class ou interface. A palavra-chave enum em Java sinaliza ao compilador Java que essa definição de tipo é um enum.
Você pode se referir às constantes no enum acima assim:
Level level = Level.HIGH;Observe como a variável level é do tipo Level que é o tipo enum Java definido no exemplo acima. A variável level pode tomar uma das Level constantes enum como valor ( HIGH, MEDIUM ou LOW). Nesse caso, level é definido como HIGH.
public enum OpcoesMenu {
SALVAR(1), IMPRMIR(2), ABRIR(3), VISUALIZAR(4), FECHAR(5);
private final int valor;
OpcoesMenu(int valorOpcao){
valor = valorOpcao;
}
public int getValor(){
return valor;
}
}https://www.devmedia.com.br/tipos-enum-no-java/25729
Pilha de Execução
class TestePilha {
public static void main(String[] args) {
System.out.println("inicio do main");
metodo1();
System.out.println("fim do main");
}
static void metodo1() {
System.out.println("inicio do metodo1");
metodo2();
System.out.println("fim do metodo1");
}
static void metodo2() {
System.out.println("inicio do metodo2");
int[] array = new int[10];
for (int i = 0; i < 10; i++) {
array[i] = i;
System.out.println(i);
}
System.out.println("fim do metodo2");
}
}- O método
mainchamametodo1 - O método
metodo1chama ometodo2
Cada um desses métodos pode ter suas próprias variáveis locais, sendo que, por exemplo, o metodo1 não enxerga as variáveis declaradas dentro do main
Toda invocação de método é empilhada em uma estrutura de dados que isola a área de memória de cada um. Quando um método termina (retorna), ele volta para o método que o invocou. Ele descobre isso através da pilha de execução (stack)
Exercícios
PodCast
Referências
Programação Orientada a Objetos
Paradigmas
Paradigmas de Programação são abordagens ou estilos diferentes de escrever código para resolver problemas.
A programação estruturada divide o código em estruturas para armazenar dados e funções que executam tarefas específicas. Ela normalmente é indicada para problemas menores e menos complexos.
Na POO, o código é organizado em objetos que representam entidades do mundo real, com atributos e métodos relacionados. Isso permite a modelagem de problemas complexos de forma mais intuitiva e promove a reutilização de código.
Exemplo
Imagine um sistema de controle de contas correntes.
Como seria escrito a definição de uma conta corrente em programção estruturada?
struct Conta{
float saldo;
int numero_conta;
char[30] correntista;
}como seria fazer um saque ou um depósito?
int sacar(struct Conta minha_conta, float valor){
if(minha_conta.saldo>=valor){
minha_conta.saldo-=valor;
return 1;
}
return 0;
}
int depositar(struct Conta minha_conta, float valor){
minha_conta.saldo+=valor;
return 1;
}Como fazer um saque quando a conta for com limite (cheque especial)?
int sacarComLimite(struct Conta minha_conta, float valor, float limite){
if(minha_conta.saldo>=valor+limite){
minha_conta.saldo-=valor;
return 1;
}
return 0;
}Objeto
- Um objeto é cada uma das entidades identificáveis num dado domínio de aplicação
- Em um sistema Bancário teríamos objetos do tipo: Cliente, Conta, Conta Corrente, Dependente, etc.
- Um objeto também pode ser visto como um agregado de outros objetos (suas partes)
- Um Objeto é uma entidade independente que armazena dados, encapsula serviços, troca mensagens com outros objetos e é modelado para executar as funções do sistema
- Um Objeto pode ser descrito pela identificação de dois elementos básicos: estrutura e comportamento
Exemplo
Objeto do tipo Pessoa
- Estrutura : nome, cpf, idade
- Comportamento : trabalhar, descansar
Exemplo
Objeto do tipo Conta
- Estrutura: titular, código, saldo
- Comportamento: debitarValor, adicionarValor
Mensagens
- São estímulos enviados aos objetos solicitando que alguma operação seja realizada por um dado objeto
- Nome da mensagem
- Parâmetros
- Especifica O QUE deve ser feito
- O comportamento de um objeto é dado pelo conjunto de mensagens que ele pode responder
Características dos Objetos
- Único
- Possui atributos que definem caraterísticas e/ou estado
- Possuem capacidade de realizar ações que chamamos de métodos ou funções
- Normalmente se diz que um objeto é uma instância de uma Classe.
- O que é uma Classe ?
Classe
Origem do termo
- A palavra classe vem da taxonomia da biologia.
- Todos os seres vivos de uma mesma classe biológica têm uma série de atributos e comportamentos em comum, mas não são iguais, podem variar nos valores desses atributos e como realizam esses comportamentos.
- Homo Sapiens define um grupo de seres que possuem características em comum
- Homo Sapiens é um ser humano?
- Tudo está especificado na classe Homo Sapiens, mas se quisermos mandar alguém correr, comer, pular, precisaremos de uma instância de Homo Sapiens, ou então de um objeto do tipo Homo Sapiens.
Analogias
- Uma receita de bolo.
- Você come uma receita de bolo?
- Precisamos instaciá-la, criar um objeto bolo a partir dessa especificação (a classe) para utilizá-la.
- Podemos criar centenas de bolos a partir dessa classe (a receita, no caso), eles podem ser bem semelhantes, alguns até idênticos, mas são objetos diferentes.
- Você come uma receita de bolo?
- A planta de uma casa é uma casa?...
Conceito
- Uma classe é uma descrição de um conjunto que compartilham os mesmos atributos(características), operações, relacionamentos e semântica
- Todos os objetos são instâncias de classes, onde a classe descreve as propriedades e comportamentos daquele objeto
- Atributos são propriedades de uma classe, que descreve um intervalo de valores que as instâncias podem apresentar. Uma Classe pode ter qualquer número de atributos ou nenhum
- Operações correspondem aos processos que a classe pode realizar
- Estrutura (molde) que define os atributos e/ou estados de um conjunto de objetos com características similares.
- Define o comportamento de seus objetos (ações que o objeto pode fazer) através de métodos.
- Descreve os serviços (ações) providos por seus objetos
- Quais informações eles podem armazenar
class Conta{
int numero;
String cliente;
double saldo;
double limite;
}Usando a classe
class Programa{
public static void main(String[] args){
new Conta();
}
}- Objeto criado, mas como acessar?
class Programa{
public static void main(String[] args){
Conta minhaConta;
minhaConta = new Conta();
}
}- Através da variável minhaConta, podemos acessar o objeto recém criado para alterar seu cliente, seu saldo, etc
class Programa{
public static void main(String[] args){
Conta minhaConta;
minhaConta = new Conta();
minhaConta.cliente = "Leandro";
minhaConta.saldo = 10.0;
System.out.println("Saldo atual: "+minhaConta.saldo);
}
}Atributos de uma Classe
- Caraterísticas e/ou estado de uma classe
- Após a classe ser instanciada em um objeto os atributos vão receber valores (caraterísticas e/ou estados) que definem o objeto
class Conta{
int numero;//atributo
String cliente;//atributo
double saldo;//atributo
double limite;//atributo
}Métodos de uma Classe
- Conjunto de ações que um determinado objeto pode executar
- Definem o que um objeto pode fazer
- São acionados por outros objetos
- Os objetos se comunicam através de métodos
- Troca de mensagens
Métodos sem retorno
- Um método que saca uma determinada quantidade e devolve nenhuma informação para quem acionar esse método
class Conta{
int numero;
String cliente;
double saldo;
double limite;
void saca(double quantidade){//método
double novoSaldo = saldo - quantidade;
saldo = novoSaldo;
}
}- Fazer um depósito
class Conta{
int numero;
String cliente;
double saldo;
double limite;
void saca(double quantidade){//método
double novoSaldo = saldo - quantidade;
saldo = novoSaldo;
}
void deposita(double quantidade){//método
saldo += quantidade;
}
}class Programa{
public static void main(String[] args){
Conta minhaConta;
minhaConta = new Conta();
minhaConta.cliente = "Leandro";
minhaConta.saldo = 100.0;
//saca 20
minhaConta.saca(20);
//deposita 50
minhaConta.deposita(50);
System.out.println("Saldo atual: "+minhaConta.saldo);
}
}Método com retorno
- No caso do nosso método saca, podemos devolver um valor booleano indicando se a operação foi bem sucedida.
class Conta{
//...
boolean saca(double valor){
if(saldo<valor){
return false;
}else{
saldo -= valor;
return true;
}
}
}class Programa{
public static void main(String[] args){
//...
minhaConta.saldo = 100.0;
boolean consegui=minhaConta.saca(20);
if(consegui){
System.out.println("Consegui sacar");
}else{
System.out.println("Não consegui sacar");
}
}
}Referência ao Objeto
class Programa{
public static void main(String[] args){
Conta c1;
c1 = new Conta();
Conta c2;
c2 = new Conta();
}
}- c1 uma variável que "aponta" para o objeto(referência).
class Programa{
public static void main(String[] args){
Conta c1;
c1 = new Conta();
Conta c2 = c1;
}
}?
Como seria a transferência de valores entre duas contas?
Details
void transferir(Conta destino, double quantidade){
if(saca(quantidade)){
destino.deposita(quantidade);
}
}Comparando
public static void main(String args[]) {
Conta c1 = new Conta();
c1.cliente = "Leandro";
c1.saldo = 100.0;
Conta c2 = new Conta();
c2.cliente = "Leandro";
c2.saldo = 100.0;
if (c1 == c2) {
System.out.println("Contas iguais");
}
}- O operador
==compara o conteúdo das variáveis- variáveis não guardam o objeto, e sim o endereço em que ele se encontra (referência)
- As contas podem ser equivalentes no nosso critério de igualdade, porém elas não são o mesmo objeto.
equals
public static void main(String args[]) {
Conta c1 = new Conta();
c1.cliente = "Leandro";
c1.saldo = 100.0;
Conta c2 = new Conta();
c2.cliente = "Leandro";
c2.saldo = 100.0;
if (c1.equals(c2)) {
System.out.println("Contas iguais");
}
}class Conta {
//...
public boolean equals(Conta outraConta) {
return numero == outraConta.numero;
}
//...
}Transformando o objeto em String
toString
O método toString() em Java é um método da classe Object (falaremos de herança mais para frente) que retorna uma representação em formato de string do objeto em questão.
Se uma classe em Java deseja ter sua própria representação em formato de string, ela pode sobrescrever esse método e fornecer uma implementação personalizada. A implementação sobrescrita deve retornar uma string que descreva o objeto de uma forma útil e significativa para o usuário.
Por exemplo, a seguinte classe Pessoa sobrescreve (falaremos de herança mais para frente) o método toString() para fornecer uma representação personalizada de uma pessoa:
public class Pessoa {
private String nome;
private int idade;
public Pessoa(String nome, int idade) {
nome = nome;
idade = idade;
}
@Override
public String toString() {
return "Pessoa{" +
"nome='" + nome + "'" +
", idade=" + idade + "}";
}
}Neste exemplo, o método toString() retorna uma string formatada que inclui o nome e a idade da pessoa em questão, com o seguinte formato: Pessoa{nome='Alice', idade=30}.
Para usar o método toString() em uma instância da classe Pessoa, basta chamá-lo em uma referência para um objeto Pessoa, como no exemplo abaixo:
Pessoa person = new Pessoa("Alice", 30);
String personString = person.toString(); // retorna "Pessoa{nome='Alice', idade=30}"
System.out.println(person);//Escreve Pessoa{nome='Alice', idade=30} no consoleExercícios
Referências
Construtor
- Método especial definido na classe e executado no momento que o objeto é instanciado
- Diferente de outro método pois não possui retorno
- Deve ter o mesmo nome da classe.
- Pode receber parâmetros
- Normalmente utilizados para inicializar os valores dos atributos do objeto
class Conta{
int numero;
String cliente;
double saldo;
double limite;
Conta(){
}
void saca(double quantidade){
double novoSaldo = this.saldo - quantidade;
this.saldo = novoSaldo;
}
void deposita(double quantidade){
this.saldo += quantidade;
}
}O que o new faz?
- A classe chamada é instanciada
- Memória é alocada
- Os passos definidos dentro do método construtor da classe são executados
- Construtor é um método especial para criar e inicializar novas instâncias da classe.
- Construtores podem ser sobrecarregados
class Conta{
//...
Conta(){
this.limite = 100;
}
//...
}Sobrecarga
É a capacidade de definir métodos com o mesmo nome
- Assinatura seja diferente.
- A mudança na assinatura ocorre alterando a quantidade e/ou tipo de parâmetros que um método recebe
Sobrecarga é a capacidade de um objeto responder à mesma mensagem, com comportamentos (métodos) distintos, a depender dos tipos dos parâmetros recebidos
- aplicarInjecao()
- aplicarInjecao(String nomeRemedio)
//...
public int somar(int v1, int v2){
return v1 + v2;
}
public int operar(int v1, int v2){
return operar('+', v1, v2);
}
public int operar(char op, int v1, int v2){
switch(op){
case '+':
return somar(v1, v2);
break;
case '-':
return subtrair(v1, v2);
}
}
//...Sobrecarga de construtores
class Conta{
//...
Conta(int numero, String cliente){
this.numero = numero;
this.cliente = cliente;
this.saldo = 0;
this.limite = 0;
}
Conta(int numero, String cliente, double saldo, double limite){
this(numero, cliente);
this.saldo = saldo;
this.limite = limite;
}
//...
}class Programa{
public static void main(String[] args){
Conta minhaConta1;
minhaConta1 = new Conta(1, "Leandro1");
minhaConta1.saldo = 100;
Conta minhaConta2;
minhaConta2 = new Conta(2, "Leandro2", 100, 0);
}
}UML
Estereótipo de uma Classe em UML
Estrutura básica de uma classe
public class Carro {
private String cor;
private String marca;
private int velocidade;
public Carro(String cor, String marca){
this.cor = cor;
this.marca = marca;
velocidade = 0;
}
public void acelerar(){
velocidade++;
}
public void parar(){
velocidade = 0;
}
}class Programa{
public static void main(String[] args){
Carro c1 = new Carro("vermelha","BMW");
//Carro c2 = new Carro();// ERRO
}
}Representação UML
A UML é uma notação que podemos utilizar para representar classes e objetos em modelos computacionais
Linguagem para representação de modelos visuais com um significado especifico e padronizado
UML não é uma linguagem de programação
Os modelos são representados através de diagramas que possuem semântica própria
O diagrama que representa a descrição das classes é o Diagrama de Classes
Domínio de Aplicação
- Um domínio é composto pelas entidades, informações e processos relacionados a um determinado contexto.
- Uma aplicação pode ser desenvolvida para automatizar ou tornar factível as tarefas de um domínio.
- Portanto, uma aplicação é basicamente o "reflexo" de um domínio.
- Para exemplificar, suponha que estamos interessados em desenvolver uma aplicação para facilitar as tarefas do cotidiano de um banco. Podemos identificar clientes, funcionários, agências e contas como entidades desse domínio. Assim como podemos identificar as informações e os processos relacionados a essas entidades.
Outras classes do domínio de um sistema bancário
Exercícios
Associações
- Forma como uma classe se relaciona com outra classe
- Uma classe pode conter atributos que geram instâncias de outra classe
- Uma classe pode conter outra classe como atributo
- Quando isto ocorre dizemos que uma classe possui outra classe associada a ela
Agregação
A classe contida não é instanciada no escopo da classe principal
- Não depende da principal para existir
- Normalmente é passada por parâmetro
Agregação é uma associação em que um objeto é parte de outro, de tal forma que a parte pode existir sem o todo.
Em mais baixo nível, uma agregação consiste de um objeto contendo referências para outros objetos, de tal forma que o primeiro seja o todo, e que os objetos referenciados sejam as partes do todo.
Composição
- A classe contida é instanciada pela classe principal
- Quando uma instancia da classe principal é retirada da memória, as instancias das outras classes também são.
- O todo contém as partes (e não referências para as partes). Quando o todo desaparece, todas as partes também desaparecem.
Exercícios
Trabalhando com ArrayList
Antes de chegarmos em toda a hierarquia das Collections vamos falar e utilizar um pouco o ArrayList
Adicionando elementos em uma lista
Para criar um objeto do tipo ArrayList, certamente fazemos como sempre: utilizando o operador new. Mas repare que acabamos passando um pouco mais de informações. Ao declarar a referência a uma ArrayList, passamos qual o tipo de objeto com o qual ela trabalhará. Se queremos uma lista de nomes de aulas, vamos declarar ArrayList<String>. Crie a classe TestandoListas, adicionando os nomes de algumas aulas que teremos nesse curso:
import java.util.List;
import java.util.ArrayList;
public class TestandoListas {
public static void main(String[] args) {
String aula1 = "Modelando a classe Aula";
String aula2 = "Conhecendo mais de listas";
String aula3 = "Trabalhando com Cursos e Sets";
ArrayList<String> aulas = new ArrayList<>();
aulas.add(aula1);
aulas.add(aula2);
aulas.add(aula3);
System.out.println(aulas);
}
}Qual é o resultado desse código? Ele mostra as aulas adicionadas em sequência! Por que isso acontece? Pois a classe ArrayList, ou uma de suas mães, reescreveu o método toString, para que internamente fizesse um for, concatenando os seus elementos internos separados por vírgula.
Removendo elementos
Bastante simples! O que mais podemos fazer com uma lista? As operações mais básicas que podemos imaginar, como por exemplo remover um determinado elemento. Usamos o método remove e depois mostramos o resultado para ver que a primeira foi removida:
aulas.remove(0);
System.out.println(aulas);Por que 0? Pois as listas, assim como a maioria dos casos no Java, são indexadas a partir do 0, e não do 1.
Percorrendo uma lista
Bem, talvez não seja a melhor das ideias fazer um System.out.println na nossa lista, pois talvez queiramos mostrar esses itens de alguma outra forma, como por exemplo um por linha. Como fazer isso? Utilizando o for de uma maneira especial, popularmente foreach. Lembrando que foreach não existe no Java como comando, e sim como um caso especial do for mesmo. Olhe o código:
for (String aula : aulas) {
System.out.println("Aula: " + aula);
}contains
O método contains é utilizado para verificar se um determinado elemento está presente na lista. Ele retorna true se o elemento estiver na lista e false caso contrário. O método utiliza o equals para comparar os elementos, então é importante que a classe dos objetos dentro da lista implemente corretamente o método equals. Veja o exemplo:
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
class Aluno {
private String nome;
private String matricula;
public Aluno(String matricula, String nome) {
this.matricula = matricula;
this.nome = nome;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Aluno)) return false;
Aluno aluno = (Aluno) obj;
return Objects.equals(matricula, aluno.matricula);
}
}
public class TestandoListas {
public static void main(String[] args) {
List<Aluno> alunos = new ArrayList<>();
Aluno aluno1 = new Aluno("A1","João");
Aluno aluno2 = new Aluno("A2","Maria");
alunos.add(aluno1);
alunos.add(aluno2);
Aluno aluno3 = new Aluno("A1","João");
System.out.println(alunos.contains(aluno3)); // true, pois aluno3 é igual a aluno1
}
}Acessando elementos
E se eu quisesse saber apenas a primeira aula? O método aqui é o get. Ele retorna o primeiro elemento se passarmos o 0 como argumento:
String primeiraAula = aulas.get(0);
System.out.println("A primeira aula é " + primeiraAula);Você pode usar esse mesmo método para percorrer a lista toda, em vez do foreach. Para isso, precisamos saber quantos elementos temos nessa lista. Nesse caso, utilizamos o método size para limitar o nosso for:
for (int i = 0; i < aulas.size(); i++) {
System.out.println("aula : " + aulas.get(i));
}Mais uma forma de percorrer elementos, agora com Java 8
Uma outra forma de percorrer nossa lista é utilizando as sintaxes e métodos novos incluídos no Java 8. Temos um método (não um comando!) agora que se chama forEach. Ele recebe um objeto do tipo Consumer, mas o interessante é que você não precisa criá-lo, você pode utilizar uma sintaxe bem mais enxuta, mas talvez assustadora a primeira vista, chamada lambda. Repare:
aulas.forEach(aula -> {
System.out.println("Percorrendo:");
System.out.println("Aula " + aula);
});Exemplo sistema banco
class Conta {
int numero;
String cliente;
double saldo;
double limite;
Conta(int numero, String cliente) {
if (numero < 0) {
this.numero = 999;
} else {
this.numero = numero;
}
setCliente(cliente);
this.saldo = 0;
this.limite = 100;
}
Conta(int numero, String cliente, double saldo, double limite) {
this(numero, cliente);
this.saldo = saldo;
this.limite = limite;
}
void setCliente(String cliente) {
if (cliente != null && !cliente.isEmpty() && !cliente.isBlank()) {
this.cliente = cliente;
}else{
this.cliente = "GERENTE";
}
}
boolean saca(double quantidade) {// método
if (this.saldo < quantidade) {
return false;
} else {
this.saldo -= quantidade;
return true;
}
}
void deposita(double quantidade) {// método
this.saldo += quantidade;
}
boolean transferir(Conta destino, double valor) {
if (this.saca(valor)) {
destino.deposita(valor);
return true;
}
return false;
}
double getSaldo() {
return saldo;
}
int getNumero() {
return numero;
}
@Override
public String toString() {
return "Conta [numero=" + numero + ", cliente=" + cliente + ", saldo=" + saldo + "]";
}
}
import java.util.ArrayList;
class Agencia {
int numero;
ArrayList<Conta> contas;
Agencia(int numero) {
this.numero = numero;
contas = new ArrayList<>();
}
int getNumero() {
return numero;
}
int criarConta(String cliente){
//calcula o numero da nova conta
int numeroConta = numero*100;
numeroConta+= contas.size();
//instancia nova conta com o numero calculado
Conta novConta = new Conta(numeroConta, cliente);
//guardo nova conta na minha lista de contas
contas.add(novConta);
//devolvo a conta para quem pediu
return novConta.getNumero();
}
int totalContas() {
return contas.size();
}
double totalDinheiro() {
double total = 0;
for (int i = 0; i < contas.size(); i++) {
total+= contas.get(i).getSaldo();
}
return total;
}
Conta getConta(int numeroConta) {
// buscar a conta que tem o numero igual a numeroConta
for (int i = 0; i < contas.size(); i++) {
Conta c = contas.get(i);
if(c.getNumero() == numeroConta){
return c;
}
}
return null;
}
}
class App {
public static void main(String[] args) throws Exception {
Agencia ag1 = new Agencia(2);
int numeroConta1 = ag1.criarConta("Leandro");
int numeroConta2 = ag1.criarConta("Isabela");
Conta conta1 = ag1.getConta(numeroConta1);
Conta conta2 = ag1.getConta(numeroConta2);
//Somente mesmo pacote pode chamar o new
//Conta conta3 = new Conta(0, null);// erro
conta1.deposita(100.0);
conta2.deposita(10.0);
conta1.transferir(conta2, 50);
System.out.println(conta1.toString());
System.out.println(conta2);
System.out.println("ag1.totalContas():"+ag1.totalContas());
System.out.println("ag1.totalDinheiro():"+ag1.totalDinheiro());
}
}Get com listas
public ArrayList<Conta> getContas() {
return contas;
}
//main
agencia.getContas().add(new Conta())//?
import java.util.List;
//...
public List<Conta> getContas() {
return List.copyOf(contas);
}
//main
agencia.getContas().add(new Conta())//?Associações com listas
Referências
Encapsulamento
- Separar o programa em partes, tornando cada parte mais isolada possível uma da outra
- A ideia é tornar o software mais flexível, fácil de modificar e de criar novas implementações
- Permite utilizar o objeto de uma classe sem necessariamente conhecer sua implementação
- Protege o acesso direto aos atributos de uma instância fora da classe onde estes foram criados
- Uma grande vantagem do encapsulamento é que toda parte encapsulada pode ser modificada sem que os usuários da classe em questão sejam afetados
Pacotes
- Forma de organizar classes dentro de uma estrutura de árvores.
- Podemos entender a estrutura de árvores como os diretórios do sistema operacional.
- O nome completo de uma classe é definido pelo seu pacote e o nome.
- Organiza suas classes e bibliotecas
- Os diretórios estão diretamente relacionados aos chamados pacotes e costumam agrupar classes de funcionalidade parecida
- No pacote java.util por exemplo, temos as classes Date, SimpleDateFormat e GregorianCalendar; todas elas trabalham com datas de formas diferentes
- Significa que essas classes estão no diretório java/util/
- A palavra chave package indica qual pacote que contém a classe
package java.util;- Para usar uma classe ou um pacote você precisa usar a import palavra-chave:
import pacote.Class; // Importa uma única classe
import pacotenovo.*; // Importa todas as classes do pacote- O nome da classe na verdade para o compilador é
- java.util.Date
- java.util.SimpleDateFormat
- java.util.GregorianCalendar
- java.io.File
Importar uma classe
Se você encontrar uma classe que deseja usar, por exemplo, a classe Scanner, que é usada para obter a entrada do usuário, escreva o seguinte código:
import java.util.Scanner;No exemplo acima, java.util é um pacote, enquanto Scanner é uma classe do pacote java.util.
Para usar a classe Scanner, crie um objeto da classe e use qualquer um dos métodos disponíveis encontrados na documentação da classe Scanner. Em nosso exemplo, usaremos o método nextLine(), que é usado para ler uma linha completa:
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Informe o nome");
String nome = scanner.nextLine();
System.out.println("nome é : " + nome);
}
}Usar uma classe sem import
Para utilizar uma classe sem a palavra reservada import você pode referenciar a classe pelo nome completo (pacote.nomeClasse). Exemplo:
class Main {
public static void main(String[] args) {
java.util.Scanner scanner = new java.util.Scanner(System.in);
System.out.println("Informe o nome");
String nome = scanner.nextLine();
System.out.println("nome é : " + nome);
}
}Modificadores de acesso
- private
- protected
- public
- <padrão> (package, quando não é especificado nenhum dos 3 acima)
São aplicados a atributos, métodos, construtores e classes
As classes só podem ser declaradas como public ou padrão
- Uma classe com acesso padrão só pode ser detectada por classes do mesmo pacote
- Uma classe com acesso público pode ser detectada por classes de todos os pacotes
Private
- Os membros privados só podem ser acessados por um código da mesma classe
Protected
- Os membros protegidos podem ser acessados por outras classes do mesmo pacote, além de subclasses independente do pacote
Public
- Os membros públicos podem ser acessados por todas as outras classes, mesmo de pacotes diferentes
Padrão
- Os membros padrão só podem ser acessados por outras classes do mesmo pacote
Métodos de acesso (get e set)
Como os atributos/métodos privados só podem ser acessadas dentro da mesma classe (uma classe externa não tem acesso a ela) é possível acessá-los se fornecermos métodos públicos get e set.
O get retorna o valor da variável e o set define o valor.
A sintaxe para ambos é que eles começam com get ou set seguido pelo nome do atributo com a primeira letra em maiúscula:
public class Pessoa {
private String nome; // private = acesso restrito
// Get
public String getNome() {
return nome;
}
// Set
public void setNome(String novoNome) {
if(novoNome!= null && !novoNome.isEmpty() && !novoNome.isBlank()){
this.nome = novoNome;
}
}
}O método get retorna o valor da variável name.
O método set pega um parâmetro ( novoNome) e o atribui ao atributo nome.
A palavra-chave this é usada para se referir ao objeto atual.
No entanto, como o atributo name é declarada como private, não podemos acessá-la de fora desta classe:
public class Main {
public static void main(String[] args) {
Pessoa pessoa = new Pessoa();
pessoa.nome = "João"; // error
System.out.println(pessoa.nome); // error
}
}Se o atributo foi declarada como public, esperaríamos a seguinte saída:
JoãoNo entanto, ao tentar acessar um atributo private, obtemos um erro:
Main.java:4: error: nome has private access in Pessoa
pessoa.nome = "João";
^
Main.java:5: error: nome has private access in Pessoa
System.out.println(pessoa.nome);
^
2 errorsEm vez disso, usamos os métodos getNome() e setNome() para acessar e atualizar a variável:
Exemplo
public class Main {
public static void main(String[] args) {
Pessoa pessoa = new Pessoa();
pessoa.setNome("João"); // Seta o valor do atributo nome para "João"
System.out.println(pessoa.getNome());
}
}saida
"João"Por que encapsulamento?
- Melhor controle dos atributos e métodos da classe
- Os atributos de classe podem ser somente leitura (se você usar apenas o método get) ou somente gravação (se você usar apenas o método set)
- Flexível: o programador pode alterar uma parte do código sem afetar outras partes
- Maior segurança de dados
Exercícios
API de data do Java
As entidades
O primeiro passo é conhecer os tipos de entidades que a API de data traz suporte. Segue uma lista resumida .
Um ponto importante: a API traz representações que cobrem dia, mês, dia da semana, ano, timezone, offset, além da combinação de todas elas!
DayOfWeek dayOfWeek = DayOfWeek.MONDAY;
Month month = Month.JANUARY;
MonthDay monthDay = MonthDay.now();
YearMonth yearMonth = YearMonth.now();
Year year = Year.now();
LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();
OffsetDateTime offsetDateTime = OffsetDateTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.now();
Clock clock = Clock.systemUTC();
Instant instant = Instant.now();
TimeZone timeZone = TimeZone.getDefault();
System.out.println("DayOfWeek: " + dayOfWeek);
System.out.println("month: " + month);
System.out.println("MonthDay: " + monthDay);
System.out.println("YearMonth: " + yearMonth);
System.out.println("Year: " + year);
System.out.println("LocalDate: " + localDate);
System.out.println("LocalTime: " + localTime);
System.out.println("LocalDateTime: " + localDateTime);
System.out.println("OffsetDateTime: " + offsetDateTime);
System.out.println("ZonedDateTime: " + zonedDateTime);
System.out.println("Clock: " + clock.getZone());
System.out.println("Instant: " + instant);
System.out.println("TimeZone: " + timeZone.getDisplayName());Formatando o LocalDateTime
LocalDateTime agora = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d/M/y h:m:s");
String agoraFormatado = agora.format(formatter);
System.out.println("LocalDateTime formatado: " + agoraFormatado);Nesse memento, o que é feito a criação da cada tipo.
Salientamos aqui as possibilidades que podem ser utilizadas com tipos ou entidades que representam tempo na API, ao invés de usar apenas um único, como era realizado anteriormente. Por exemplo, o YearMonth para trabalhar com a data de validade de um cartão de crédito ou Year para lidar com o ano de publicação de um livro.
Essas variáveis, além de trazer uma melhor semântica para dentro do código, trazem clareza e simplificam como você realiza as validações. Isso é baseado no clássico e já mencionado “When Make a Type” do Martin Fowler.
É possível também realizar combinações entre tipos para criar uma instância final, por exemplo, começamos com ano e vamos até à data.
LocalDate myBirthday = Year.of(1988).atMonth(Month.JANUARY).atDay(9);Assim, como primeiro passo, sugiro que você explore e leia um pouco mais sobre os tipos que a API suporta. Essas APIs são muito mais eficientes para representar o tempo que tipos mais genéricos como int, long ou String, já que a API de data possui validações temporáveis.
Operações com data
Além de trazer mais semântica e validação para lidar com datas, a API também traz algumas operações bem interessantes que visam facilitar o seu dia, além de salvar o seu tempo.
As operações mais básicas são as de adicionar ou remover períodos, como mês ou dias, através dos métodos com os sufixos plus ou minus respectivamente.
LocalDate myBirthday = Year.of(1988).atMonth(Month.JANUARY).atDay(9);
LocalDate yesterday = myBirthday.minusDays(1);
LocalDate oneYear = myBirthday.plusDays(365);A interface TemporalAdjuster permite ajustes customizados e algumas operações mais complexas e a partir dela é possível criar várias operações customizáveis e reutilizáveis. Como convenção, essa classe utilitária traz diversos recursos, por exemplo, verificar a próxima segunda-feira a partir da data atual, me refiro a classe TemporalAdjusters.
LocalDate myBirthday = Year.of(1988).atMonth(Month.JANUARY).atDay(9);
LocalDate newYear = myBirthday.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDayOfMonth = myBirthday.with(TemporalAdjusters.lastDayOfMonth());
LocalDate nextMonday = myBirthday.with(TemporalAdjusters.next(DayOfWeek.MONDAY));É possível também realizar comparações entre datas, nada muito recente comparado as API mais antigas, no entanto, é sempre importante salientar que isso existe.
LocalDate myBirthday = Year.of(1988).atMonth(Month.JANUARY).atDay(9);
LocalDate now = LocalDate.now();
Assertions.assertTrue(now.isAfter(myBirthday));
Assertions.assertFalse(now.isBefore(myBirthday));As operações básicas são bem úteis, porém, é possível ir além com o Period e também ChronoUnit. Com eles, você consegue ver a diferença entre um determinado período.
LocalDate today = LocalDate.now();
LocalDate tomorrow = LocalDate.now().plusDays(1);
Assertions.assertEquals(1, ChronoUnit.DAYS.between(today, tomorrow));
LocalDate dateA = LocalDate.of(2012, Month.APRIL, 7);
LocalDate dateB = LocalDate.of(2015, Month.DECEMBER,5);
Period period = Period.between(dateA, dateB);
Assertions.assertEquals(3, period.getYears());
Assertions.assertEquals(7, period.getMonths());
Assertions.assertEquals(28, period.getDays());
Assertions.assertEquals(43, period.toTotalMonths());O último passo para comparação e verificação é o TemporalQuery, que tem o objetivo de extrair alguma informação do tempo.
TemporalQuery<Boolean> weekend = temporal -> {
int dayOfWeek = temporal.get(ChronoField.DAY_OF_WEEK);
return dayOfWeek == DayOfWeek.SATURDAY.getValue()
|| dayOfWeek == DayOfWeek.SUNDAY.getValue();
};
LocalDate date = LocalDate.of(2018, 5, 4);
LocalDateTime sunday = LocalDateTime.of(2018, 5, 6, 17, 0);
Assertions.assertFalse(date.query(weekend));
Assertions.assertTrue(sunday.query(weekend));O ponto de destaque de recursos de operações de data certamente é a possibilidade de trabalhar com os timezones.
ZoneId saoPaulo = ZoneId.of("America/Sao_Paulo");
ZonedDateTime adenNow = ZonedDateTime.now(saoPaulo);
ZoneOffset offset = adenNow.getOffset();
Assertions.assertEquals(saoPaulo, adenNow.getZone());
Assertions.assertEquals("-03:00", offset.getId());É possível realizar comparações de timezones diferentes, por exemplo, comparar um horário do Brasil e outro de Portugal, que no dia 3 de maio tem a diferença de quatro horas.
ZoneId saoPaulo = ZoneId.of("America/Sao_Paulo");
ZoneId portugal = ZoneId.of("Portugal");
LocalDateTime timeSP = Year.of(2021).atMonth(Month.MAY).atDay(3)
.atTime(12,0,0);
LocalDateTime timeLisbon = Year.of(2021).atMonth(Month.MAY).atDay(3)
.atTime(16,0,0);
ZonedDateTime zoneSaoPaulo = ZonedDateTime.of(timeSP, saoPaulo);
ZonedDateTime zoneLisbon = ZonedDateTime.of(timeLisbon, portugal);
Assertions.assertTrue(zoneSaoPaulo.isEqual(zoneLisbon));Comparando timezones de São Paulo e Portugal e achando sua equivalência de três horas no período.
É importante reforçar que o que mencionamos é apenas uma visão geral da API e vale muito ler a documentação do projeto, que é extremamente rica e detalhada.
Herança
A herança é um dos pilares da programação orientada a objetos e representa a capacidade de uma classe reutilizar a estrutura e o comportamento definidos em outra classe, chamada de superclasse ou classe pai. A classe que herda essas características é chamada de subclasse ou classe filha.
Com a herança, uma subclasse passa a incorporar tudo o que a superclasse possui — atributos e métodos — podendo ainda acrescentar suas próprias características específicas. Esse mecanismo permite a criação de classes genéricas que reúnem um conjunto de definições comuns a diversos objetos (processo conhecido como generalização). A partir dessas classes genéricas, é possível desenvolver outras mais específicas, que complementam e estendem os comportamentos existentes (o que chamamos de especialização).
Em Java, a herança é simples, ou seja, uma classe só pode herdar de uma única superclasse — herança múltipla não é permitida. O principal objetivo desse recurso é especializar uma classe, enriquecendo-a com novas funcionalidades sem a necessidade de reescrever código já existente.
Enquanto a especialização permite que uma classe se torne mais detalhada e específica, a generalização atua no sentido oposto, tornando o modelo mais abstrato e amplo, adequado a um número maior de situações.
Tips
- É a capacidade de uma classe definir o seu comportamento e sua estrutura aproveitando definições de outra classe, normalmente conhecida como classe base ou classe pai
- As subclasses herdam tudo o que a classe pai possui e acrescenta as suas características particulares
- Através do mecanismo de herança é possível definirmos classes genéricas que agreguem um conjunto de definições comuns a um grande número de objetos(Generalização)
- A partir destas especializações genéricas podemos construir novas classes, mais específicas, que acrescentem novas características e comportamentos aos já existentes (Especialização)
- Capacidade que uma classe tem de herdar as características e comportamentos de outra classe
- Classe pai é chamada de superclasse e a filha de subclasse
- Em Java só é permitido herdar de uma única classe, ou seja, não permite herança múltipla
- O objetivo da herança é especializar o entendimento de uma classe criando novas características e comportamentos que vão além da superclasse
- Ao mesmo tempo que a especialização amplia o entendimento de uma classe, a generalização vai no sentido inverso e define um modelo menos especializado e mais genérico
public class Mamifero{
private int altura;
private double peso;
public void mamar(){
IO.println("Mamifero mamando");
}
}public class Morcego extends Mamifero{
private double tamanhoPresa;
public void voar(){
IO.println("Morcego voando");
}
}- Classe Morcego
- Quais as características de morcego?
- altura
- peso
- tamanhoPresa
- Quais ações o morcego pode fazer?
- mamar
- voar
- Quais as características de morcego?
- Se usarmos os princípios de lógica podemos dizer que todo morcego é mamífero porém NÃO podemos afirmar que todo mamífero é morcego
Mamifero animalMamifero = new Morcego();
Morcego batman = new Mamifero();//erro- Com base no que foi dito até aqui podemos deduzir que o item 2 deve causar um erro já que não é possível garantir que todo mamífero seja um morcego
- Já o item 1 pode parecer estranho, pois a variável é do tipo
Mamiferoe o objeto para o qual a variável se refere é do tipoMorcego- Devemos saber que toda variável pode receber um objeto que seja compatível com o seu tipo e neste caso todo Morcego CERTAMENTE é um Mamífero
Mamifero animalMamifero = new Morcego();
animalMamifero.mamar();
animalMamifero.voar();//erro- Todo
Morcegoé umMamifero, porem não pode realizar todas as ações de ummorcego - A variável
animalMamiferoque recebe o objeto é do tipoMamifero - Para o
Morcegovoaré necessário criar uma nova variável do tipoMorcegoe atribuir o objeto que estava na variávelanimalMamifero
Mamifero animalMamifero = new Morcego();
animalMamifero.mamar();
Morcego batman = (Morcego)animalMamifero;
batman.voar();- Este tipo de operação recebe o nome de TYPE CAST
Outros exemplos
Caelum
Como toda empresa, nosso Banco possui funcionários. Vamos modelar a classe Funcionario:
class Funcionario {
String nome;
String cpf;
double salario;
// métodos devem vir aqui
}Além de um funcionário comum, há também outros cargos, como os gerentes. Os gerentes guardam a mesma informação que um funcionário comum, mas possuem outras informações, além de ter funcionalidades um pouco diferentes. Um gerente no nosso banco possui também uma senha numérica que permite o acesso ao sistema interno do banco, além do número de funcionários que ele gerencia:
class Gerente {
String nome;
String cpf;
double salario;
int senha;
int numeroDeFuncionariosGerenciados;
public boolean autentica(int senha) {
if (this.senha == senha) {
IO.println("Acesso Permitido!");
return true;
} else {
IO.println("Acesso Negado!");
return false;
}
}
// outros métodos
}Precisamos mesmo de outra classe?
Poderíamos ter deixado a classe Funcionario mais genérica, mantendo nela senha de acesso, e o número de funcionários gerenciados. Caso o funcionário não fosse um gerente, deixaríamos estes atributos vazios.
Essa é uma possibilidade, porém podemos começar a ter muito atributos opcionais, e a classe ficaria estranha. E em relação aos métodos? A classe Gerente tem o método autentica, que não faz sentido existir em um funcionário que não é gerente
Se tivéssemos um outro tipo de funcionário que tem características diferentes do funcionário comum, precisaríamos criar uma outra classe e copiar o código novamente!
Além disso, se um dia precisarmos adicionar uma nova informação para todos os funcionários, precisaremos passar por todas as classes de funcionário e adicionar esse atributo. O problema acontece novamente por não centralizarmos as informações principais do funcionário em um único lugar!
Existe um jeito, em Java, de relacionarmos uma classe de tal maneira que uma delas herda tudo que a outra tem. Isto é uma relação de classe mãe e classe filha. No nosso caso, gostaríamos de fazer com que o Gerente tivesse tudo que um Funcionario tem, gostaríamos que ela fosse uma extensão de Funcionario. Fazemos isto através da palavra chave extends.
class Gerente extends Funcionario {
int senha;
int numeroDeFuncionariosGerenciados;
public boolean autentica(int senha) {
if (this.senha == senha) {
IO.println("Acesso Permitido!");
return true;
} else {
IO.println("Acesso Negado!");
return false;
}
}
// setter da senha omitido
}Em todo momento que criarmos um objeto do tipo Gerente, este objeto possuirá também os atributos definidos na classe Funcionario, pois um Gerente é um Funcionario:
class TestaGerente {
public static void main(String[] args) {
Gerente gerente = new Gerente();
// podemos chamar métodos do Funcionario:
gerente.setNome("João da Silva");
// e também métodos do Gerente!
gerente.setSenha(4231);
}
}Dizemos que a classe Gerente herda todos os atributos e métodos da classe mãe, no nosso caso, a Funcionario. Para ser mais preciso, ela também herda os atributos e métodos privados, porém não consegue acessá-los diretamente. Para acessar um membro privado na filha indiretamente, seria necessário que a mãe expusesse um outro método visível que invocasse esse atributo ou método privado.
Super e Sub classe
A nomenclatura mais encontrada é que Funcionario é a superclasse de Gerente, e Gerente é a subclasse de Funcionario. Dizemos também que todo Gerente é um Funcionario. Outra forma é dizer que Funcionario é classe mãe de Gerente e Gerente é classe filha de Funcionario.
E se precisamos acessar os atributos que herdamos? Não gostaríamos de deixar os atributos de Funcionario public, pois dessa maneira qualquer um poderia alterar os atributos dos objetos deste tipo. Existe um outro modificador de acesso, o protected, que fica entre o private e o public. Um atributo protected só pode ser acessado (visível) pela própria classe e por suas subclasses (e mais algumas outras classes, mas veremos isso mais adiante).
class Funcionario {
protected String nome;
protected String cpf;
protected double salario;
// métodos devem vir aqui
}Sempre usar protected?
Então porque usar private? Depois de um tempo programando orientado a objetos, você vai começar a sentir que nem sempre é uma boa ideia deixar que a classe filha acesse os atributos da classe mãe, pois isso quebra um pouco a ideia de que só aquela classe deveria manipular seus atributos. Essa é uma discussão um pouco mais avançada.
Além disso, não só as subclasses, mas também as outras classes, podem acessar os atributos protected, que veremos mais a frente (mesmo pacote).
Da mesma maneira, podemos ter uma classe Diretor que estenda Gerente e a classe Engenheiro pode estender diretamente de Funcionario.
Fique claro que essa é uma decisão de negócio. Se Diretor vai estender de Gerente ou não, vai depender se, para você, Diretor é um Gerente.
Uma classe pode ter várias filhas, mas pode ter apenas uma mãe, é a chamada herança simples do java.
K19
Reutilização de Código
Um banco oferece diversos serviços que podem ser contratados individualmente pelos clientes. Quando um serviço é contratado, o sistema do banco deve registrar quem foi o cliente que contratou o serviço, quem foi o funcionário responsável pelo atendimento ao cliente e a data de contratação.
Com o intuito de ser produtivo, a modelagem dos serviços do banco deve diminuir a repetição de código. A ideia é reaproveitar o máximo do código já criado. Essa ideia está diretamente relacionada ao conceito Don’t Repeat Yourself. Em outras palavras, devemos minimizar ao máximo a utilização do "copiar e colar". O aumento da produtividade e a diminuição do custo de manutenção são as principais motivações do DRY.
Em seguida, vamos discutir algumas modelagens possíveis para os serviços do banco. Buscaremos seguir a ideia do DRY na criação dessas modelagens.
Uma classe para todos os serviços
Poderíamos definir apenas uma classe para modelar todos os tipos de serviços que o banco oferece.
class Servico {
private Cliente contratante ;
private Funcionario responsavel ;
private LocalDate dataDeContratacao ;
// métodos
}Empréstimo
O empréstimo é um dos serviços que o banco oferece. Quando um cliente contrata esse serviço, são definidos o valor e a taxa de juros mensal do empréstimo. Devemos acrescentar dois atributos na classe Servico: um para o valor e outro para a taxa de juros do serviço de empréstimo.
class Servico {
// GERAL
private Cliente contratante;
private Funcionario responsavel;
private LocalDate dataDeContratacao;
// EMPRÉSTIMO
private double valor;
private double taxa;
// métodos
}Seguro de veículos
Outro serviço oferecido pelo banco é o seguro de veículos. Para esse serviço devem ser definidas as seguintes informações: veículo segurado, valor do seguro e a franquia. Devemos adicionar três atributos na classe Servico.
class Servico {
// GERAL
private Cliente contratante ;
private Funcionario responsavel ;
private LocalDate dataDeContratacao ;
// EMPRÉSTIMO
private double valor ;
private double taxa ;
// SEGURO DE VEICULO
private Veiculo veiculo ;
private double valorDoSeguroDeVeiculo ;
private double franquia ;
// métodos
}Apesar de seguir a ideia do DRY, modelar todos os serviços com apenas uma classe pode dificultar o desenvolvimento. Supondo que dois ou mais desenvolvedores são responsáveis pela implementação dos serviços, eles provavelmente modificariam a mesma classe concorrentemente. Além disso, os desenvolvedores, principalmente os recém chegados no projeto do banco, ficariam confusos com o código extenso da classe Servico.
Outro problema é que um objeto da classe Servico possui atributos para todos os serviços que o banco oferece. Na verdade, ele deveria possuir apenas os atributos relacionados a um serviço. Do ponto de vista de performance, essa abordagem causaria um consumo desnecessário de memória.
Uma classe para cada serviço
Para modelar melhor os serviços, evitando uma quantidade grande de atributos e métodos desnecessários, criaremos uma classe para cada serviço.
class SeguroDeVeiculo {
// GERAL
private Cliente contratante ;
private Funcionario responsavel ;
private LocalDate dataDeContratacao ;
// SEGURO DE VEICULO
private Veiculo veiculo ;
private double valorDoSeguroDeVeiculo ;
private double franquia ;
// métodos
}class Emprestimo {
// GERAL
private Cliente contratante ;
private Funcionario responsavel ;
private LocalDate dataDeContratacao ;
// EMPRÉSTIMO
private double valor ;
private double taxa ;
// métodos
}Criar uma classe para cada serviço torna o sistema mais flexível, pois qualquer alteração em um determinado serviço não causará efeitos colaterais nos outros. Mas, por outro lado, essas classes teriam bastante código repetido, contrariando a ideia do DRY. Além disso, qualquer alteração que deva ser realizada em todos os serviços precisa ser implementada em cada uma das classes.
Uma classe genérica e várias específicas
Na modelagem dos serviços do banco, podemos aplicar um conceito de orientação a objetos chamado Herança. A ideia é reutilizar o código de uma determinada classe em outras classes.
Aplicando herança, teríamos a classe Servico com os atributos e métodos que todos os serviços devem ter e uma classe para cada serviço com os atributos e métodos específicos do determinado serviço.
As classes específicas seriam "ligadas" de alguma forma à classe Servico para reaproveitar o código nela definido. Esse relacionamento entre as classes é representado em UML pelo diagrama abaixo
Os objetos das classes específicas Emprestimo e SeguroDeVeiculo possuiriam tanto os atributos e métodos definidos nessas classes quanto os definidos na classe Servico.
Emprestimo e = new Emprestimo() ;
// Chamando um método da classe Servico
e.setDataDeContratacao(LocalDate.of(2020, Month.JANUARY, 8)) ;
// Chamando um método da classe Emprestimo
e.setValor(10000) ;As classes específicas são vinculadas a classe genérica utilizando o comando extends. Não é necessário redefinir o conteúdo já declarado na classe genérica.
class Servico {
private Cliente contratante;
private Funcionario responsavel;
private LocalDate dataDeContratacao;
}class Emprestimo extends Servico {
private double valor;
private double taxa;
}class SeguroDeVeiculo extends Servico {
private Veiculo veiculo;
private double valorDoSeguroDeVeiculo;
private double franquia;
}A classe genérica é denominada super classe, classe base ou classe mãe. As classes específicas são denominadas sub classes, classes derivadas ou classes filhas.
Quando o operador new é aplicado em uma sub classe, o objeto construído possuirá os atributos e métodos definidos na sub classe e na super classe.
package heranca;
import java.time.LocalDate;
public class Servico {
private Cliente contratante;
private Funcionario responsavel;
private LocalDate dataDeContratacao;
public Servico(Cliente contratante, Funcionario responsavel,
LocalDate dataDeContratacao) {
this.contratante = contratante;
this.responsavel = responsavel;
this.dataDeContratacao = dataDeContratacao;
}
public Cliente getContratante() {
return contratante;
}
public Funcionario getResponsavel() {
return responsavel;
}
public LocalDate getDataDeContratacao() {
return dataDeContratacao;
}
}package heranca;
import java.io.IO;
import java.time.LocalDate;
public class Emprestimo extends Servico {
private double valor;
private double taxa;
public Emprestimo(Cliente contratante, Funcionario responsavel,
LocalDate dataDeContratacao,double valor,double taxa) {
super(contratante, responsavel, dataDeContratacao);
this.valor = valor;
this.taxa = taxa;
}
@Override
public String toString() {
return "Emprestimo [valor=" + valor + ", taxa=" + taxa + ", getContratante()=" + getContratante()
+ ", getResponsavel()=" + getResponsavel() + ", getDataDeContratacao()=" + getDataDeContratacao() + "]";
}
public static void main(String[] args) {
Cliente c1 = new Cliente();
Funcionario f1 = new Funcionario("Leandro F", "0000000000", 100);
LocalDate data = LocalDate.of(2022,2,25);
Emprestimo e = new Emprestimo(c1, f1, data, 10000, 1);
IO.println(e);
}
}Construtores e Herança
Construtor Padrão
Em Java, toda classe herda implicitamente da classe Object, que é a superclasse raiz de todas as classes. A classe Object possui um construtor padrão (sem parâmetros), que é chamado automaticamente se nenhuma outra chamada ao construtor da superclasse for especificada.
Exemplo do Construtor Padrão da Classe Object
Embora não seja visível diretamente, a classe Object contém um construtor público:
public class Object {
public Object() {
// Construtor padrão vazio
}
}Quando criamos uma classe sem definir explicitamente um construtor, o compilador Java adiciona automaticamente um construtor sem argumentos que chama o construtor da superclasse:
class MinhaClasse {
// Construtor padrão gerado implicitamente pelo compilador
public MinhaClasse() {
super(); // Chama o construtor da classe Object
}
}Quando temos uma hierarquia de classes, as chamadas dos construtores são mais complexas do que o normal. Pelo menos um construtor de cada classe de uma mesma sequência hierárquica deve ser chamado ao instanciar um objeto. Por exemplo, quando um objeto da classe Emprestimo é criado, pelo menos um construtor da própria classe Emprestimo e um da classe Servico devem ser executados. Além disso, os construtores das classes mais genéricas são chamados antes dos construtores das classes específicas.
class Servico {
// ATRIBUTOS
public Servico(){
IO.println("Servico");
}
}class Emprestimo extends Servico {
// ATRIBUTOS
public Emprestimo(){
IO.println("Emprestimo");
}
}Por padrão, todo construtor chama o construtor sem argumentos da classe mãe se não existir nenhuma chamada de construtor explícita.
class TesteConstrutor {
public static void main(String[] args) {
new Emprestimo();
}
}Construtores com Parâmetros
Construtores com parâmetros são utilizados para inicializar os atributos de um objeto no momento da criação. Se uma classe filha definir um construtor, ela deve chamar explicitamente o construtor da superclasse usando super() para garantir que a inicialização da classe base ocorra corretamente.
Exemplo: Classe Pai e Classe Filha
// Classe Pai
class Pessoa {
String nome;
// Construtor com parâmetro
public Pessoa(String nome) {
this.nome = nome;
IO.println("Construtor da classe Pessoa chamado!");
}
}
// Classe Filha
class Aluno extends Pessoa {
int matricula;
// Construtor da classe filha
public Aluno(String nome, int matricula) {
super(nome); // Chama o construtor da classe Pai (Pessoa)
this.matricula = matricula;
IO.println("Construtor da classe Aluno chamado!");
}
}
// Classe Principal para Teste
public class Main {
public static void main(String[] args) {
Aluno aluno = new Aluno("Carlos", 12345);
}
}Saída do Código:
Construtor da classe Pessoa chamado!
Construtor da classe Aluno chamado!Por que a Classe Filha Deve Chamar super()?
Inicialização Correta da Superclasse
- Como a classe
Alunoherda dePessoa, a parte referente aPessoaprecisa ser inicializada antes que os atributos específicos deAlunosejam definidos.
- Como a classe
Garantia de Consistência
- Se a classe
Pessoapossui lógica importante no seu construtor (como validação de dados ou inicialização de recursos), essa lógica precisa ser executada antes que o objetoAlunoseja utilizado.
- Se a classe
Evita Erros de Compilação
- Se a classe pai não tiver um construtor sem parâmetros e a classe filha não chamar explicitamente
super(args), o código não compilará.
- Se a classe pai não tiver um construtor sem parâmetros e a classe filha não chamar explicitamente
O Que Acontece se super() Não For Chamado?
Se a classe Pessoa não tivesse um construtor padrão (sem parâmetros), o seguinte código geraria erro de compilação:
class Aluno extends Pessoa {
int matricula;
// Erro: Pessoa(String) deve ser chamado explicitamente
public Aluno(int matricula) {
this.matricula = matricula;
}
}Erro de compilação:
"Constructor Pessoa in class Pessoa cannot be applied to given types"
Isso acontece porque o Java sempre tenta chamar super() implicitamente se nenhum construtor da superclasse for especificado. Como Pessoa não tem um construtor padrão, o compilador não consegue encontrar um construtor adequado para ser chamado automaticamente.
Sobrescrita de Métodos
Caelum
Todo fim de ano, os funcionários do nosso banco recebem uma bonificação. Os funcionários comuns recebem 10% do valor do salário e os gerentes, 15%.
Vamos ver como fica a classe Funcionario:
class Funcionario {
protected String nome;
protected String cpf;
protected double salario;
public double getBonificacao() {
return this.salario * 0.10;
}
// métodos
}Se deixarmos a classe Gerente como ela está, ela vai herdar o método getBonificacao.
Gerente gerente = new Gerente();
gerente.setSalario(5000.0);
IO.println(gerente.getBonificacao());O resultado aqui será 500. Não queremos essa resposta, pois o gerente deveria ter 750 de bônus nesse caso. Para consertar isso, uma das opções seria criar um novo método na classe Gerente, chamado, por exemplo, getBonificacaoDoGerente. O problema é que teríamos dois métodos em Gerente, confundindo bastante quem for usar essa classe, além de que cada um da uma resposta diferente.
No Java, quando herdamos um método, podemos alterar seu comportamento. Podemos reescrever (reescrever, sobrescrever, override) este método:
class Gerente extends Funcionario {
int senha;
int numeroDeFuncionariosGerenciados;
public double getBonificacao() {
return this.salario * 0.15;
}
// ...
}Agora o método está correto para o Gerente. Refaça o teste e veja que o valor impresso é o correto 750:
Gerente gerente = new Gerente();
gerente.setSalario(5000.0);
IO.println(gerente.getBonificacao());A anotação @Override
Há como deixar explícito no seu código que determinador método é a reescrita de um método da sua classe mãe. Fazemos isso colocando @Override em cima do método. Isso é chamado anotação. Existem diversas anotações e cada uma vai ter um efeito diferente sobre seu código.
@Override
public double getBonificacao() {
return this.salario * 0.15;
}Perceba que, por questões de compatibilidade, isso não é obrigatório. Mas caso um método esteja anotado com @Override, ele necessariamente precisa estar reescrevendo um método da classe mãe.
Invocando o método reescrito
Depois de reescrito, não podemos mais chamar o método antigo que fora herdado da classe mãe: realmente alteramos o seu comportamento. Mas podemos invocá-lo no caso de estarmos dentro da classe.
Imagine que para calcular a bonificação de um Gerente devemos fazer igual ao cálculo de um Funcionario porem adicionando R$ 1000. Poderíamos fazer assim:
class Gerente extends Funcionario {
int senha;
int numeroDeFuncionariosGerenciados;
public double getBonificacao() {
return this.salario * 0.10 + 1000;
}
// ...
}Aqui teríamos um problema: o dia que o getBonificacao do Funcionario mudar, precisaremos mudar o método do Gerente para acompanhar a nova bonificação. Para evitar isso, o getBonificacao do Gerente pode chamar o do Funcionario utilizando a palavra chave super.
class Gerente extends Funcionario {
int senha;
int numeroDeFuncionariosGerenciados;
public double getBonificacao() {
return super.getBonificacao() + 1000;
}
// ...
}Essa invocação vai procurar o método com o nome getBonificacao de uma super classe de Gerente. No caso ele logo vai encontrar esse método em Funcionario.
Essa é uma prática comum, pois muitos casos o método reescrito geralmente faz "algo a mais" que o método da classe mãe. Chamar ou não o método de cima é uma decisão sua e depende do seu problema. Algumas vezes não faz sentido invocar o método que reescrevemos.
K19
Suponha que o valor da taxa administrativa do serviço de empréstimo é diferente dos outros serviços, pois ele é calculado a partir do valor emprestado ao cliente. Como esta lógica é específica para o serviço de empréstimo, devemos acrescentar um método para implementar esse cálculo na classe Emprestimo.
class Emprestimo extends Servico {
// ATRIBUTOS
public double calculaTaxaDeEmprestimo(){
return this.valor * 0.1;
}
}Para os objetos da classe Emprestimo, devemos chamar o método calculaTaxaDeEmprestimo().
Para todos os outros serviços, devemos chamar o método calculaTaxa().
Mesmo assim, nada impediria que o método calculaTaxa() fosse chamado em um objeto da
classe Emprestimo, pois ela herda esse método da classe Servico. Dessa forma, existe o risco de alguém erroneamente chamar o método incorreto.
Seria mais seguro "substituir" a implementação do método calculaTaxa() herdado da classeServico na classe Emprestimo. Para isso, basta escrever o método calculaTaxa() também na classe Emprestimo com a mesma assinatura que ele possui na classe Servico.
class Emprestimo extends Servico {
// ATRIBUTOS
public double calculaTaxa(){
return this.valor * 0.1;
}
}Os métodos das classes específicas têm prioridade sobre os métodos das classes genéricas. Em outras palavras, se o método chamado existe na classe filha ele será chamado, caso contrário o método será procurado na classe mãe.
Quando definimos um método com a mesma assinatura na classe base e em alguma classe derivada, estamos aplicando o conceito de Reescrita de Método.
Fixo + Específico
Suponha que o preço de um serviço é a soma de um valor fixo mais um valor que depende do tipo
do serviço. Por exemplo, o preço do serviço de empréstimo é 5 reais mais uma porcentagem do valor emprestado ao cliente. O preço do serviço de seguro de veículo é 5 reais mais uma porcentagem do valor do veículo segurado. Em cada classe específica, podemos reescrever o método calculaTaxa().
class Emprestimo extends Servico {
// ATRIBUTOS
public double calculaTaxa(){
return 5 + this.valor * 0.1;
}
}class SeguraDeVeiculo extends Servico {
// ATRIBUTOS
public double calculaTaxa(){
return 5 + this.veiculo.getTaxa()* 0.05;
}
}Se o valor fixo dos serviços for atualizado, todas as classes específicas devem ser modificadas. Outra alternativa seria criar um método na classe Servico para calcular o valor fixo de todos os serviços e chamá-lo dos métodos reescritos nas classes específicas.
class Servico {
public double calculaTaxa(){
return 5 ;
}
}class Emprestimo extends Servico {
// ATRIBUTOS
public double calculaTaxa(){
return super.calculaTaxa()+ this.valor * 0.1;
}
}Dessa forma, quando o valor padrão do preço dos serviços é alterado, basta modificar o método
na classe Servico.
Proibindo Herança
Para proibir que uma classe seja estendida, podemos usar o modificador de acesso final. Quando uma classe é declarada como final, ela não pode ser estendida por outras classes.
Exemplo de Classe Final
// Classe final
final class ClasseFinal {
public void metodo() {
IO.println("Método da classe final");
}
}
// Tentativa de estender a classe final
class SubClasse extends ClasseFinal { // Isso causará um erro de compilação
public void metodo() {
IO.println("Método da subclasse");
}
}Limitando Herança
Classes seladas são um recurso presente em várias linguagens orientadas a objetos modernas (Java, Kotlin, C#, entre outras)[17][18][19][20]. Elas permitem que o desenvolvedor controle explicitamente quais subclasses podem estender uma determinada classe ou interface, limitando a herança a um conjunto pré-definido de tipos.
Para definir quais classes podem estender outra, usa-se a palavra-chave sealed em conjunto com a palavra-chave permits na definição da classe que será estendida. A classe selada (sealed class) lista explicitamente as classes que têm permissão para estendê-la, restringindo a herança apenas a essas classes especificadas.
Exemplo de Classe Selada
// Classe selada
sealed class ClasseSelada permits SubClasse1, SubClasse2 {
public void metodo() {
IO.println("Método da classe selada");
}
}
// Subclasse permitida
final class SubClasse1 extends ClasseSelada {
public void metodo() {
IO.println("Método da SubClasse1");
}
}
// Outra subclasse permitida
final class SubClasse2 extends ClasseSelada {
public void metodo() {
IO.println("Método da SubClasse2");
}
}
// Tentativa de estender a classe selada por uma classe não permitida
class SubClasseNaoPermitida extends ClasseSelada { // Isso causará um erro de compilação
public void metodo() {
IO.println("Método da SubClasseNaoPermitida");
}
}Pontos Positivos
- Encapsulamento reforçado: Ao restringir a herança, as classes seladas ajudam a manter a integridade da implementação original, evitando que subclasses externas modifiquem ou quebrem invariantes importantes do sistema. Isso é especialmente útil para proteger componentes críticos em frameworks e bibliotecas[18:1][21][22].
- Menor acoplamento com o mundo externo: Uma vez que apenas tipos conhecidos podem estender a classe, o sistema limita dependências externas e torna o comportamento mais previsível e fácil de raciocinar. O controle sobre o polimorfismo se torna explícito, dificultando efeitos colaterais indesejados[19:1][23][21:1].
- Facilita manutenção e refatoração: Saber exatamente quais classes compõem uma hierarquia torna mudanças futuras menos arriscadas, pois o impacto é restrito ao conjunto selado de subclasses[19:2][20:1].
- Aprimora type safety e pattern matching: Nas linguagens que suportam pattern matching, como Kotlin e Java, classes seladas permitem checagem exaustiva de tipos pelo compilador, sendo possível garantir que todo fluxo de decisão cobre todos os casos possíveis, sem a necessidade de um
elsegenérico[19:3][20:2][21:2]. - Expressa melhor a intenção de design: O uso de classes seladas comunica claramente aos demais desenvolvedores e ao compilador quais são as extensões permitidas, evitando ambiguidades[17:1][21:3][22:1].
- Otimizações de desempenho: Em compiladores, saber que uma classe não será herdada pode permitir otimizações internas[24][25].
Pontos Negativos
- Flexibilidade reduzida: Selar uma classe impede a extensão fora do escopo permitido, o que pode ser um problema se, no futuro, surgirem novos requisitos de evolução modular ou reuso por herança[19:4][25:1][22:2]. Decisões prematuras podem prejudicar a extensibilidade.
- Acoplamento interno cresce: O núcleo da hierarquia (dentro do módulo/pacote) torna-se mais acoplado, pois todas as possíveis variações precisam ser previstas e implementadas antecipadamente pelo autor da classe base, limitando customizações externas[26].
- Risco de overengineering: O uso exagerado desse recurso pode tornar o design do sistema excessivamente rígido, dificultando adaptações, manutenção e entendimento do código em longo prazo[19:5][27].
- Dificuldade de depuração: Em alguns casos, o uso extensivo pode dificultar teste e depuração, já que não é possível substituir facilmente a lógica com subclasses específicas para fins de mocking ou ajuste[25:2].
- Impactos em padrões de projeto tradicionais: Padrões como "Inversion of Control" e o uso de interfaces para desacoplamento podem não funcionar com classes fortemente seladas, exigindo abordagens alternativas[26:1].
Boas Práticas
- Use classes seladas em hierarquias fechadas, onde as possíveis variações são bem conhecidas e improváveis de mudar[17:2][19:6][20:3][22:3].
- Evite selar classes prematuramente; pense no contexto e na possibilidade de extensão futura antes de aplicar esse nível de restrição[22:4].
- Documente sempre o motivo pelo qual a classe é selada, comunicando claramente a intenção do design[22:5].
- Para desacoplamento e extensão, priorize interfaces não-seladas e composição sobre herança rígida, mantendo o sistema maleável quando necessário[26:2].
Referências
Geenfoot
Interagindo com o Greenfoot
Este tutorial usa um cenário chamado "wombats", que você pode baixar aqui (também está incluído nos cenários de exemplo com versões do Greenfoot anteriores à 2.4.0). Abra o cenário "wombats" no Greenfoot; você deverá ver isto:

Se você não vir o mundo e as classes à direita estiverem com barras diagonais sobre elas, é porque o código não está compilado. Clique no botão "Compilar" no canto inferior direito.
A grande área da grade que cobre a maior parte da janela é chamada de mundo. Como temos um cenário aqui relacionado a wombates, vemos um mundo de wombates. À direita da janela está a classe display. Aqui você pode ver todas as classes Java envolvidas no projeto. As classes World e Ator estarão sempre lá — elas vêm com o sistema Greenfoot. As outras classes pertencem ao cenário de wombates e serão diferentes se você usar cenários diferentes.
Abaixo do mundo estão os Controles de Execução (a área com os botões Act, Run, Reset e Speed o controle deslizante).

Coloque objetos no mundo
Agora, vamos inserir alguns objetos no mundo. Clique com o botão direito do mouse na classe Wombat na exibição de classes. Você verá um menu pop-up como este:

Escolha new Wombat() no menu. Em seguida, clique em qualquer lugar do mundo. Você acabou de criar um wombat (em termos Java: um objeto) e inseri-lo no mundo.
Wombats comem folhas, então vamos colocar algumas folhas no mundo também. Clique com o botão direito do mouse na classe Leaf, selecione new Leaf() e coloque a folha no mundo.
Existe um atalho para posicionar vários objetos um pouco mais rápido: Shift+clique no mundo. Certifique-se de que a classe Leaf esteja selecionada (clique com o botão esquerdo do mouse no painel de classes e ela ficará com uma borda preta mais grossa), então mantenha pressionada a tecla Shift e clique com o botão esquerdo do mouse no mundo várias vezes. Você posicionará um objeto da classe selecionada a cada clique. Muito mais rápido!
Faça os objetos agirem
Clique no botão Act nos controles de execução. Cada objeto agora age — ou seja: cada objeto faz o que foi programado para fazer. No nosso exemplo, as folhas são programadas para não fazer nada, enquanto os Wombates são programados para se moverem para frente. Tente colocar dois wombates no mundo e pressione Act novamente. Ambos se moverão.
Os wombates também gostam de comer folhas. Se encontrarem uma folha no caminho, eles a comerão. Tente colocar algumas folhas na frente dos wombates e clique em Act — os wombates se moverão para frente e comerão as folhas.
Execute um cenário
Clique no botão Run. Isso equivale a clicar no botão Act repetidamente, muito rapidamente. Você notará que o botão Run muda para um botão Pause; clicar em Pause interrompe toda a ação.
O controle deslizante ao lado dos botões Act e Run define a velocidade. Clique em Run e altere o controle deslizante, e você verá a diferença.
Invocar métodos diretamente
Em vez de simplesmente executar o cenário inteiro, você também pode invocar métodos individuais. Um método é uma ação única que um objeto pode executar.
Certifique-se de que haja um wombate no mundo e que o cenário não esteja em execução. Em seguida, clique com o botão direito do mouse no wombate (aquele no mundo, não na classe Wombat) e você verá que os objetos no mundo também têm um menu pop-up:

Você pode selecionar qualquer um dos métodos mostrados aqui para pedir ao vombate que faça algo. Experimente, por exemplo, turnLeft(). Selecionar isso no menu diz ao vombate para virar para a esquerda. Experimente também move().
Alguns métodos fornecem uma resposta. getLeavesEaten(), por exemplo, informará quantas folhas este vombate comeu até o momento. Experimente. Depois, faça o vombate comer outra folha e tente chamar esse método novamente.
Você também notará um método chamado act(). Este método é chamado sempre que você clica no botão "Agir". Se quiser que apenas um objeto atue em vez de todos os objetos do mundo, você pode fazer isso invocando o método act() do objeto diretamente.
Crie um novo mundo
Se você tiver muitos objetos no mundo que não deseja mais e quiser começar tudo de novo, há uma opção fácil: descartar o mundo e criar um novo. Isso geralmente é feito clicando no botão "Redefinir" nos controles de execução. Você obterá um novo mundo vazio. O mundo antigo é descartado (e com ele todos os objetos que estavam nele) — você só pode ter um mundo ativo por vez.
Invocar um método de World
Vimos que objetos no mundo possuem métodos que você pode invocar por meio de um menu pop-up. O mundo em si também é um objeto com métodos que você pode invocar. Clique com o botão direito do mouse em qualquer espaço vazio no mundo, ou na área cinza imediatamente ao lado do mundo, e você verá o menu do mundo:

Um dos métodos neste menu é populate(). Experimente. É um método que cria várias folhas e wombates e os coloca no mundo. Você pode então executar o cenário.
Outro método de mundo é randomLeaves(int howMany). Este método coloca algumas folhas no mundo em locais aleatórios. Observe que este método possui algumas palavras entre parênteses após seu nome: int howMany. Isso é chamado de parâmetro. Isso significa que você deve especificar alguma informação adicional ao invocar este método. O termo int indica que um número inteiro é esperado, e o nome howMany sugere que você deve especificar quantas folhas deseja. Invoque este método. Uma caixa de diálogo será exibida, permitindo que você insira um valor para este parâmetro. Insira um número (por exemplo: 12) e clique em Ok.
(Você pode notar, se contar, que às vezes parece que menos folhas do que o número especificado foram criadas. Isso ocorre porque algumas folhas podem estar no mesmo local e sobrepostas.)
Movimento e Controle de Teclas
Vamos fazer movimento no Greenfoot e como controlar atores com o teclado.
O Cenário dos Caranguejos
Baixe o arquivo zip do cenário inicial do Crab e descompacte o conteúdo em algum lugar do seu disco rígido. Em seguida, abra o cenário nesse local no Greenfoot; você deverá ver a interface padrão do Greenfoot, com um mundo arenoso vazio:

Clique com o botão direito na classe Crab e selecione new Crab(), depois clique com o botão esquerdo no mundo para posicionar o caranguejo:

Depois disso, clique em Executar. Você pode estar torcendo para ver o Caranguejo dançar uma dança incrível pela tela. Infelizmente, parece que temos um Caranguejo preguiçoso! Vamos abrir o código e dar uma olhada; você pode clicar duas vezes na classe Caranguejo no navegador de classes ou clicar com o botão direito e selecionar "Abrir Editor":

O que você verá é o código Java para o Crab. Você não precisa entender tudo ainda, mas a parte importante é o código entre as chaves abaixo da linha public void act() — não há nada lá:

No código, você pode ver que não há nada entre as chaves. Se quisermos que nosso caranguejo faça alguma coisa, teremos que preencher isso. Vamos colocá-lo em movimento, adicionando uma instrução de movimento ao código:
public void act(){
move(4);
}Você precisa corresponder exatamente ao que está escrito lá. É a palavra "move", seguida por parênteses curvos contendo o número 4, seguido por um ponto e vírgula. Erros comuns incluem colocar o "m" em maiúscula (letras maiúsculas importam em Java!), não usar o ponto e vírgula, usar os parênteses errados (ou pensar que parênteses curvos vazios são um zero) ou excluir acidentalmente as chaves. Se você receber um erro durante este tutorial, procure por um destes erros que você pode ter cometido ao copiar o código.
Depois de escrever isso, clique no botão Compilar na interface principal do Greenfoot (ou no topo da janela do editor), coloque um caranguejo no mundo novamente e clique em Executar. Agora, o caranguejo deve deslizar lateralmente pela tela. Em seguida, ele deve atingir a borda do mundo e parar abruptamente. Se desejar, você pode pausar o cenário, arrastar o caranguejo para a esquerda, clicar em Executar e assisti-lo novamente. Por que não colocar mais alguns caranguejos no mundo e vê-los fazer isso? Os caranguejos não estão realmente parando; eles ainda estão tentando se mover, mas o Greenfoot não os deixa sair do mundo (se deixassem, como você os arrastaria de volta?). Você pode variar a velocidade do Caranguejo alterando o número 4 no código para um número diferente. Quanto maior a velocidade, mais rápido, menor a velocidade — veja se consegue adivinhar o que acontece com um número negativo.
Vamos fazer o caranguejo fazer um pouco mais do que se mover em linha reta. Volte ao código e, após a linha de movimento, adicione outra linha (mas ainda dentro das chaves do método act) que diga turn(3), assim:
public void act(){
move(4);
turn(3);
}Você verá que o caranguejo corre em círculos. Experimente girar o círculo para torná-lo mais estreito ou mais largo — deixaremos você decidir qual direção precisa e o porquê.
A vantagem de o caranguejo girar o tempo todo é que, mesmo que bata na borda do mundo, eventualmente ele girará o suficiente para se afastar da borda e voltar para o centro. Seria ainda melhor se pudéssemos controlar a rotação do caranguejo — vamos interagir um pouco no nosso cenário! Podemos fazer o caranguejo girar pressionando as teclas de cursor (seta) para a esquerda ou para a direita. Aqui está o código:
public void act(){
move(4);
if (Greenfoot.isKeyDown("left")){
turn(-3);
}
if (Greenfoot.isKeyDown("right")){
turn(3);
}
}Usamos os métodos internos do Greenfoot para verificar se uma tecla está pressionada. Entre as aspas está o nome da tecla, "left" é a tecla do cursor para a esquerda, "right" é a tecla para a direita. Se você quiser algo como "a" e "d", basta usá-los! Nosso código está dizendo: se essas teclas estiverem pressionadas, gire um certo número de graus. Digite esse código, compile-o e experimente você mesmo. Você pode alterar a velocidade de rotação do caranguejo aumentando esses números.
Se você colocar vários caranguejos no mundo, verá que pressionar as teclas controla todos eles ao mesmo tempo em gloriosa sincronização, transformando você em uma espécie de senhor dos caranguejos: todos os caranguejos estão executando o mesmo código, então se você pressionar a tecla esquerda, todos eles verão que a tecla esquerda está pressionada e todos girarão de acordo.
O que acontece se você segurar os botões esquerdo e direito ao mesmo tempo? Experimente e descubra — depois, observe o código e tente descobrir o porquê.
Detectando e Removendo Atores e Criando Métodos
Este tutorial explicará como descobrir se você está tocando em outro ator e, posteriormente, removê-lo do mundo. Também explicará como manter seu código legível usando métodos.
Comendo minhocas
Já temos uma classe para o caranguejo — agora vamos adicionar outra para a minhoca. A minhoca também será um ator, então para criar a classe minhoca, clique com o botão direito (Mac: control-clique) na classe Ator e selecione "nova subclasse...":

Como novo nome de classe, insira: Worm — observe que estamos usando W maiúsculo. É convenção em Java que comecemos os nomes de classe com letra maiúscula. Se você acidentalmente usar um w minúsculo agora, receberá um erro mais tarde ao copiar nosso código que usa W maiúsculo. Na lista de imagens à esquerda, selecione "worm.png" como imagem e clique em OK:

Nossa classe Worm não terá código real para começar — assim como nossa classe Crab tinha quando começamos. Vamos deixar assim; nossas minhocas são burras e ficam paradas no mesmo lugar, prontas para serem comidas. Presa fácil! Vamos modificar nossa classe Crab para que, quando nossos caranguejos passarem por cima de uma minhoca, eles a comam. Para fazer isso, voltamos ao código-fonte do nosso Crab, que atualmente se parece com isto:
public void act(){
move(4);
if (Greenfoot.isKeyDown("left")){
turn(-3);
}
if (Greenfoot.isKeyDown("right")){
turn(3);
}
}No final do método act(), vamos inserir algum código para verificar se o caranguejo está tocando uma minhoca. Usaremos o método isTouching. Este método recebe um parâmetro. O parâmetro nos permite indicar em qual classe estamos interessados; essa é a classe Worm:
if(isTouching(Worm.class)){
}Só podemos remover o worm quando houver um worm:
if (isTouching(Worm.class)) {
removeTouching(Worm.class);
}Então, vamos fazer um teste. Compile, crie algumas minhocas e coloque-as no seu mundo, depois adicione um caranguejo, aperte Run e use as teclas esquerda e direita para guiar o caranguejo até as minhocas, que devem ser comidas.
public void act(){
move(4);
if (Greenfoot.isKeyDown("left")){
turn(-3);
}
if (Greenfoot.isKeyDown("right")){
turn(3);
}
if (isTouching(Worm.class)) {
removeTouching(Worm.class);
}
}Refatoração
Antes de prosseguirmos, faremos uma alteração no código. Isso é conhecido como refatoração: alterar o código sem alterar seu comportamento. Atualmente, o método run() para o Caranguejo possui dois comportamentos distintos: a metade superior lida com a movimentação e a metade inferior com a ingestão de minhocas. Seria mais claro nomeá-los e separá-los, para evitar confusões futuras. Podemos fazer isso criando métodos para cada comportamento. Na verdade, já vimos como os métodos são escritos: act() é um método, afinal.
Nossos dois novos métodos não exigem parâmetros e não retornam valor, então eles podem ser declarados da mesma forma que act(); o código ajustado está abaixo:
public void act(){
moveAndTurn();
eat();
}
public void moveAndTurn(){
move(4);
if (Greenfoot.isKeyDown("left")){
turn(-3);
}
if (Greenfoot.isKeyDown("right")){
turn(3);
}
}
public void eat(){
if (isTouching(Worm.class)) {
removeTouching(Worm.class);
}
}Se você comparar este código com a versão anterior, notará que, na verdade, não alteramos nem removemos nada do código existente. Movemos a metade superior para um novo método chamado moveAndTurn e a metade inferior para um novo método chamado eat. Em seguida, substituímos o conteúdo do método act() por chamadas para esses novos métodos. Isso tornará o código no qual nos concentraremos mais curto.
Mostrando o placar
Vamos colocar um contador no Caranguejo. Declare um int quantidade como atributo privado e inicialize ele com 0. Cada vez que o Caranguejo comer uma Warm esse numero deve ser incrementado e mostrado com o código abaixo:
//...
private int quantidade = 0;
//...
getWorld().showText(String.valueOf(quantidade), 10, 10);
//...Salvando o Mundo, Criando e Tocando Som
Como inicializar o mundo com atores, bem como reproduzir e gravar sons.
Salvando o mundo
A esta altura, você provavelmente já está cansado de ter que adicionar novos objetos ao mundo toda vez que compilamos o código. É possível adicionar código para criar automaticamente algumas minhocas e um caranguejo para você — e, além disso, é possível fazer com que o Greenfoot escreva esse código para você! Clique em Reset e adicione algumas minhocas e um caranguejo ao mundo. Antes de clicar em Run , clique com o botão direito do mouse no mundo e selecione a opção "Save the World":

Isso adiciona algum código à nossa classe CrabWorld, que criará as minhocas e o caranguejo e os adicionará ao mundo na mesma posição na próxima vez que você reiniciar ou compilar.
/**
* Constructor for objects of class CrabWorld.
*
*/
public CrabWorld()
{
super(560, 560, 1);
prepare();
}
/**
* Prepare the world for the start of the program.
* That is: create the initial objects and add them to the world.
*/
private void prepare()
{
addObject(new Crab(),169,151);
addObject(new Worm(),114,331);
addObject(new Worm(),252,269);
}Procure no código-fonte da classe CrabWorld.
Definindo o tamanho do mundo
Para alterar o tamanho do mundo, você precisa modificar o construtor da classe CrabWorld. A linha super(560, 560, 1); define o tamanho do mundo. O primeiro parâmetro é a largura, o segundo é a altura e o terceiro é o tamanho da célula em pixels.
Tocando e gravando sons
Podemos adicionar algum som ao nosso cenário. O cenário vem com um som pronto para você usar, chamado "eating.wav". Podemos fazer esse som tocar sempre que o caranguejo comer uma minhoca adicionando uma única linha à nossa classe crab que chama Greenfoot.playSound após removermos uma minhoca do mundo:
public void eat(){
if (isTouching(Worm.class)) {
removeTouching(Worm.class);
Greenfoot.playSound("eating.wav");
}
}Não se esqueça de ligar os alto-falantes (ou conectar os fones de ouvido). Uma última coisa: se você tiver um microfone no computador, pode gravar seus próprios sons. No menu Controles, há uma opção para exibir o gravador de som:

Selecione isso e você obterá o gravador de som:

Pressione o botão de gravação e fale (ou amasse um pacote de comida vazio, ou algo assim!), depois pressione parar. Você deverá ver uma onda verde e, ao pressionar play, deverá ouvir o som sendo reproduzido. Caso contrário, há um problema com o seu microfone — tente pesquisar no Google para obter ajuda com isso. Supondo que haja algum som, quase invariavelmente haverá um pouco de silêncio no início e no fim do som — você pode ver isso no visor verde, pois ele terá uma linha horizontal plana no início e no fim, antes do formato:

O silêncio no final não é um grande problema, mas o silêncio no início é irritante — significa que, quando você diz para o som tocar quando uma minhoca é comida, parece haver um pequeno atraso antes que o som comece a tocar, como se o jogo estivesse travando. Você pode limpar o silêncio selecionando a parte do meio (a parte que você deseja manter) clicando no início (após o silêncio inicial) e arrastando para o final (antes do silêncio final) — a seleção deve ser exibida em cinza. Em seguida, pressione "Cortar para seleção". O silêncio deve ser removido.
Salve o som digitando algo no campo de nome de arquivo (por exemplo, "myeating ") e clicando em Salvar. Feche o gravador de som e volte ao seu código. Encontre a linha com "eating.wav" e altere-a para "myeating.wav" (ou qualquer nome que você tenha usado, mais a extensão .wav). Assim, ao jogar, você deverá ouvir seu próprio som. Estamos quase terminando o jogo, mas precisamos adicionar um inimigo.
Criando um jogo com 2 jogadores
Vamos colocar a Lagosta para disputar com o Caranguejo.
Crie uma nova classe Lobster com o mesmo código fonte do Crab.
Para a movimentação o caranguejo utiliza as teclas da esquerda e da direita do teclado, na lagosta vamos mudar para Q e W.
public void moveAndTurn(){
move(4);
if (Greenfoot.isKeyDown("q")){
turn(-3);
}
if (Greenfoot.isKeyDown("w")){
turn(3);
}
}Podemos agora adicionar uma Lagosta e ver quem consegue pegar mais minhocas.
Usando Herança
Como o código de Crab e Lobster são muito parecidos, podemos reaproveitar boa parte do código. Isso é conhecido como herança. Vamos definir uma nova classe Decapoda que terá o que é comum a Crab e Lobster.
public class Decapoda extends Actor
{
private int quantidade = 0;
private String keyLeft = "left";
private String keyRight = "right";
private int showTextX = 10;
private int showTextY = 10;
public Decapoda(String keyLeft, String keyRight, int showTextX , int showTextY ){
this.keyLeft = keyLeft;
this.keyRight = keyRight;
this.showTextX = showTextX;
this.showTextY = showTextY;
}
public void act(){
moveAndTurn();
eat();
}
public void moveAndTurn(){
move(4);
if (Greenfoot.isKeyDown(keyLeft)){
turn(-3);
}
if (Greenfoot.isKeyDown(keyRight)){
turn(3);
}
}
public void eat(){
if (isTouching(Worm.class)) {
removeTouching(Worm.class);
Greenfoot.playSound("eating.wav");
quantidade++;
getWorld().showText(String.valueOf(quantidade), showTextX, showTextY);
}
}
}Agora vamos definir Crab e Lobster como filhas de Decapoda
public class Crab extends Decapoda
{
public Crab(){
super("left","right",10,10);
}
}public class Lobster extends Decapoda
{
public Lobster(){
super("q","w",550,550);
}
}no Greenfoot vai ficar assim:

Adicionando um inimigo que se move aleatoriamente
Depois do nosso último tutorial, agora temos um cenário com um caranguejo que podemos controlar, que corre por aí comendo minhocas. O jogo é bem fácil — embora o caranguejo seja (deliberadamente) um pouco complicado de controlar, não há tensão. O que precisamos é de um inimigo que coma caranguejos: um cavalo marinho!
Para começar, vamos adicionar um cavalo marinho que se move em linha reta e come caranguejos. Podemos fazer isso usando um código que já vimos como escrever. Primeiro, adicionamos uma classe Lobster — lembre-se de que fazemos isso clicando com o botão direito do mouse na classe Actor e selecionando a nova subclasse. Você encontrará a imagem do cavalo marinho na lista de imagens à esquerda.
Depois de criar sua classe Lobster, você pode preenchê-la fazendo-o se mover em linha reta e comendo um caranguejo, se encontrar um. Você já viu como fazer cada uma dessas coisas em tutoriais anteriores, então não vou colar o código aqui diretamente. Tente você mesmo, mas se tiver dúvidas, pode ver a resposta aqui .
Você pode testar se o cavalo marinho está funcionando colocando um à esquerda de um caranguejo, clicando em "Executar" e deixando que ambos corram para o lado direito do mundo, onde o caranguejo será comido. (Se quiser, faça seu próprio som para quando o cavalo marinho comer o caranguejo.) Isso é ótimo, mas nosso cavalo marinho é bem burro; é fácil escapar dele movendo-se para o lado, e quando ele chega ao canto direito do mundo, ele fica lá para sempre (assim como nosso caranguejo original).
Vamos tornar nosso cavalo marinho mais difícil de evitar introduzindo um pouco de aleatoriedade. O Greenfoot fornece uma função Greenfoot.getRandomNumber que fornecerá um número aleatório. Aqui está uma primeira tentativa, onde giramos uma quantidade aleatória a cada quadro:
public void moveAndTurn(){
move(4);
turn(Greenfoot.getRandomNumber(90));
}Esse código significa que giraremos uma quantidade aleatória a cada quadro, entre 0 grau (inclusive) e 90 graus (exclusive). Experimente e veja como funciona. Você verá que isso não cria um inimigo muito ameaçador: a lagosta parece girar quase sempre no mesmo lugar (girando com muita frequência, na verdade) e sempre vira para a direita. Vamos corrigir esses problemas um por um, começando pelo giro no mesmo lugar.
No momento, nossa lagosta gira a cada quadro, o que a faz girar em vez de vagar. Há algumas maneiras diferentes de fazê-la girar com menos frequência. Uma seria ter uma variável de contador que registrasse quanto tempo se passou desde a última vez que giramos, e girasse, digamos, a cada 10 quadros. Outra maneira é usar o gerador de números aleatórios para girar aleatoriamente, com uma determinada média (por exemplo, a cada 10 quadros). Usaremos outro uso do gerador aleatório, pois ele torna a lagosta menos previsível.
Digamos que uma lagosta tenha 10% de chance de virar a cada quadro. Podemos codificar isso comparando Greenfoot.getRandomNumber(100) a uma determinada porcentagem:
public void moveAndTurn(){
move(4);
if(Greenfoot.getRandomNumber(100) < 10){
turn(Greenfoot.getRandomNumber(90));
}
}Se você estiver interessado, pense cuidadosamente sobre como isso funciona — por que usamos < em vez de <=? Poderíamos ter codificado de forma diferente, por exemplo, usando Greenfoot.getRandomNumber(50) ou Greenfoot.getRandomNumber(10)? E quanto a Greenfoot.getRandomNumber(5)?
Isso fará com que nossa lagosta vire (em média) a cada 10 quadros. Compile e execute, e veja o que acontece. A lagosta deve andar principalmente em linha reta, ocasionalmente virando à direita em uma distância variável. Isso é ótimo, e nos leva de volta ao nosso outro problema: a lagosta sempre vira à direita.
Sabemos, pelo nosso caranguejo, que a maneira de virar à esquerda é usar um número negativo para o ângulo. Se pudéssemos mudar a rotação da nossa lagosta de 0 a +90 para -45 a +45, isso resolveria o nosso problema. Existem algumas maneiras diferentes de fazer isso, mas aqui está a mais simples: observe que, se subtrairmos 45 do nosso número, obtemos um número na faixa correta. Então, vamos ajustar nosso código de acordo:
public void moveAndTurn(){
move(4);
if(Greenfoot.getRandomNumber(100) < 10){
turn(Greenfoot.getRandomNumber(90)-45);
}
}Nossa amplitude de giro está perfeitamente simétrica no momento? Se não, como você poderia corrigir isso?
Compile e execute isso, e teremos um predador relativamente eficaz que pode se voltar contra você a qualquer momento. Coloque um caranguejo, várias lagostas e muitas minhocas no mundo (e salve o mundo!) e tente comer todas as minhocas antes que as lagostas o peguem. Você pode notar, no entanto, que ainda há uma falha: as lagostas podem ficar presas por um tempo nas bordas do mundo. Isso ocorre porque, uma vez que atingem a borda do mundo, elas só se afastam depois de fazerem algumas curvas aleatórias.
Podemos acelerar o processo de retirada das lagostas das paredes novamente, fazendo-as girar 180 graus assim que chegarem à borda do mundo. Podemos verificar se elas estão na borda do mundo observando se a coordenada X delas está próxima de zero ou próxima da largura do mundo — e uma lógica semelhante para a coordenada Y (e a altura do mundo). O código está abaixo:
public void moveAndTurn(){
move(4);
if(Greenfoot.getRandomNumber(100) < 10){
turn(Greenfoot.getRandomNumber(90)-45);
}
if(getX() <=5 || getX() >= getWorld().getWidth() - 5){
turn(180);
}
if(getY() <=5 || getY() >= getWorld().getHeight() - 5){
turn(180);
}
}Entrega
Criando um Jogo no Estilo Hole.io
Neste tutorial, vamos criar um jogo inspirado no popular Hole.io. O objetivo é controlar um buraco que se move pelo mundo, engolindo objetos para crescer. Quanto maior o buraco fica, maiores os objetos que ele pode engolir.
Configuração Inicial do Cenário
Primeiro, crie um novo cenário Java no Greenfoot. Vamos chamá-lo de "HoleIO".
Criando o Mundo
A primeira coisa que precisamos é de um mundo para o nosso jogo. Clique com o botão direito na classe World e selecione new subclass. Nomeie-a como MyWorld. Você pode escolher uma imagem de fundo para o seu mundo, como um asfalto ou grama. Para este exemplo, um fundo simples serve.
No construtor da classe MyWorld, defina o tamanho do mundo. Um mundo de 800x800 pixels é um bom começo.
import greenfoot.*; // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)
public class MyWorld extends World {
public MyWorld() {
// Cria um novo mundo com 800x800 células com um tamanho de célula de 1x1 pixels.
super(800, 800, 1);
}
}Criando o Buraco (Player)
Agora, vamos criar o nosso jogador. O jogador será um buraco que se move pelo cenário.
- Clique com o botão direito na classe
Actore selecionenew subclass. - Nomeie a classe como
Hole. - Escolha uma imagem para o buraco. Uma imagem simples de um círculo preto funciona perfeitamente.
Compile o cenário e coloque uma instância do Hole no mundo para ver como fica.
Movendo o Buraco com o Mouse
Diferente do jogo do caranguejo, onde usamos o teclado, aqui controlaremos o buraco usando o mouse. Queremos que o buraco siga o cursor do mouse sempre que ele estiver sobre a janela do jogo.
Abra o editor da classe Hole e adicione o seguinte código ao método act():
import greenfoot.*; // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)
public class Hole extends Actor{
public void act(){
followMouse();
}
private void followMouse(){
MouseInfo mouse = Greenfoot.getMouseInfo();
if (mouse != null) {
setLocation(mouse.getX(), mouse.getY());
}
}
}Entendendo o código:
Greenfoot.getMouseInfo(): Este método nos dá informações sobre o estado do mouse, como sua localização. Se o mouse não estiver sobre a janela do jogo, ele retornanull.if (mouse != null): Verificamos se o mouse está na janela para evitar erros.setLocation(mouse.getX(), mouse.getY()): Este comando move o atorHolepara as coordenadas X e Y do cursor do mouse.
Compile o código, coloque um Hole no mundo e clique em Run. Você verá que o buraco agora segue o seu mouse!
Criando Objetos para Engolir
Um buraco não é nada sem coisas para engolir. Vamos criar um objeto simples para começar.
- Clique com o botão direito em
Actore crie uma nova subclasse chamadaRock. - Escolha uma imagem de uma pedra para ela.
Agora, precisamos popular nosso mundo com algumas pedras. Abra a classe MyWorld e adicione um método prepare() para adicionar os objetos iniciais.
import greenfoot.*; // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)
public class MyWorld extends World{
public MyWorld(){
super(800, 800, 1);
prepare();
}
/**
* Prepara o mundo para o início do programa.
* Ou seja: cria os objetos iniciais e os adiciona ao mundo.
*/
private void prepare(){
for (int i = 0; i < 20; i++) {
int x = Greenfoot.getRandomNumber(getWidth());
int y = Greenfoot.getRandomNumber(getHeight());
addObject(new Rock(), x, y);
}
}
}- Usamos um loop
forpara criar 20 pedras. Greenfoot.getRandomNumber(getWidth())gera um número aleatório entre 0 e a largura do mundo, para que as pedras apareçam em locais aleatórios.
Compile e execute. Seu mundo agora deve estar cheio de pedras!
Mecânica de Engolir e Crescer
Agora vem a parte divertida: fazer o buraco engolir as pedras e crescer.
Volte para a classe Hole e vamos adicionar a lógica para "comer".
import greenfoot.*;
public class Hole extends Actor
{
private int size = 50; // Tamanho inicial do buraco
public Hole(){
updateImage();
}
public void act(){
followMouse();
eat();
}
private void followMouse(){
MouseInfo mouse = Greenfoot.getMouseInfo();
if (mouse != null) {
setLocation(mouse.getX(), mouse.getY());
}
}
private void eat(){
if (isTouching(Rock.class)) {
removeTouching(Rock.class);
size += 5; // Aumenta o tamanho
updateImage();
Greenfoot.playSound("eating.wav"); // Reutilizando o som do tutorial anterior
}
}
private void updateImage()
{
GreenfootImage image = new GreenfootImage("black-circle.png"); // Use o nome da sua imagem
image.scale(size, size);
setImage(image);
}
}O que há de novo:
private int size = 50;: Uma variável para rastrear o tamanho do nosso buraco.isTouching(Rock.class): Este método verifica se oHoleestá tocando em algum objeto da classeRock. Retornatruese estiver tocando, efalsecaso contrário.removeTouching(Rock.class): Se oHoleestiver tocando umRock, este método remove o objetoRockdo mundo. É uma forma conveniente de combinar a detecção e a remoção.size += 5;: A cada pedra engolida, aumentamos a variávelsize.updateImage(): Criamos um novo método para atualizar a imagem do buraco. Ele carrega a imagem original, redimensiona-a para o novosizee a aplica ao ator. Chamamos este método no construtor para definir o tamanho inicial e sempre que o buraco cresce.
Nota: Você precisará de uma imagem base para o buraco (ex: "black-circle.png") na pasta images do seu cenário.
Compile e execute. Agora, quando você passar o buraco sobre as pedras, elas desaparecerão e o buraco ficará visivelmente maior!
Adicionando Objetos Maiores e Lógica de Tamanho
O desafio do jogo vem da necessidade de crescer para engolir objetos maiores. Vamos adicionar uma nova classe, Tree.
- Crie uma nova subclasse de
ActorchamadaTree. - Escolha uma imagem de árvore para ela.
Agora, vamos modificar a lógica para que o buraco só possa engolir objetos se for grande o suficiente. Para fazer isso de forma organizada, criaremos uma classe "pai" para todos os objetos que podem ser engolidos.
- Crie uma nova subclasse de
ActorchamadaSwallowable. Não precisa de imagem. - Abra a classe
Swallowablee adicione o seguinte código:
import greenfoot.*;
public class Swallowable extends Actor
{
protected int objectSize;
}- Agora, modifique as classes
RockeTreepara que elas "herdem" deSwallowableem vez deActor.
Classe Rock:
import greenfoot.*;
public class Rock extends Swallowable
{
public Rock()
{
this.objectSize = 20; // Tamanho da pedra
}
}Classe Tree:
import greenfoot.*;
public class Tree extends Swallowable
{
public Tree()
{
this.objectSize = 80; // Tamanho da árvore (maior que o tamanho inicial do buraco)
}
}Agora, atualize o método eat() na classe Hole:
private void eat()
{
if (isTouching(Swallowable.class)) {
Swallowable item = (Swallowable) getOneIntersectingObject(Swallowable.class);
if (item!=null && this.size > item.objectSize) {
getWorld().removeObject(item);
size += 5;
updateImage();
Greenfoot.playSound("eating.wav");
}
}
}Mudanças:
- Utilizamos
isTouching(Swallowable.class)para verificar se estamos tocando em um objeto que pode ser engolido. - Em seguida, usamos
getOneIntersectingObject(Swallowable.class)para obter a referência do objeto e poder verificar seu tamanho. - A condição
ifcontinua verificando se o tamanho do buraco (this.size) é maior que o tamanho do item que ele está tentando engolir (item.objectSize).
Finalmente, adicione algumas árvores ao seu mundo no método prepare() da classe MyWorld:
private void prepare(){
// Adiciona 20 pedras
for (int i = 0; i < 20; i++) {
addObject(new Rock(), Greenfoot.getRandomNumber(getWidth()), Greenfoot.getRandomNumber(getHeight()));
}
// Adiciona 10 árvores
for (int i = 0; i < 10; i++) {
addObject(new Tree(), Greenfoot.getRandomNumber(getWidth()), Greenfoot.getRandomNumber(getHeight()));
}
}Compile e execute. Você verá que o buraco pode engolir as pedras, mas não as árvores. Conforme você engole as pedras, o buraco cresce. Quando o tamanho dele ultrapassar 80, ele será capaz de engolir as árvores também!
Próximos Passos
A partir daqui, você pode expandir o jogo de várias maneiras:
- Adicionar mais objetos: Crie classes para carros, prédios, etc., cada um com um
objectSizediferente. - Pontuação: Adicione um contador de pontos que aumenta a cada objeto engolido. Você pode exibir a pontuação na tela usando
world.showText().
Entrega
Exercício de Refatoração: Otimizando o Jogo "Hole.io" com Orientação a Objetos
Objetivo:
Após construir a primeira versão funcional do nosso jogo, o próximo passo crucial no desenvolvimento de software é a refatoração. Vamos aprimorar nosso código para aplicar os conceitos de Herança, Encapsulamento e Construtores de forma mais robusta e elegante.
O objetivo é fazer com que a classe Swallowable se torne mais inteligente e responsável, simplificando as classes filhas (Rock, Tree) e tornando nosso código mais fácil de manter e expandir.
Instruções:
Siga os passos abaixo para refatorar o código do seu projeto.
1. Refatorando a Superclasse Swallowable
Esta classe se tornará o cérebro por trás de todos os objetos que podem ser engolidos.
Encapsulamento: O atributo
objectSizenão deve ser acessível diretamente pelas classes filhas. Transforme-o emprivatee crie um método "getter" público para acessá-lo.Construtor com Parâmetros: Modifique o construtor da
Swallowablepara que ele receba o tamanho e a imagem como parâmetros. Isso permitirá que cada subclasse informe seu tamanho e qual imagem deve ser exibida no momento da criação.Responsabilidade de Dimensionamento: Um objeto deve poder chamar "desenhar" de si mesmo. A
Swallowabledeve ser responsável por aplicar oobjectSizeao tamanho visual de sua imagem. Então, vamos criar um métodoscaleImagecom uma definição de visibilidade que a subclasse possa chamar.
?
Faz sentido existir alguma instancia de Swallowable sem ser uma especialização (como Rock ou Tree)?
2. Simplificando as Subclasses (Rock e Tree)
Com a superclasse mais inteligente, as subclasses se tornam muito mais simples.
- Chamada ao
super(): O construtor deRockeTreedeve, como primeira ação, chamar o construtor da superclasse, passando seu tamanho específico e definição da imagem
3. Ajustando a Classe Hole
Finalmente, ajuste o método eat() da classe Hole para acessar o tamanho dos objetos engolíveis através do método getter que você criou na Swallowable. Isso garante que a Hole não dependa diretamente dos detalhes internos das subclasses.
O Hole deve crescer ao engolir qualquer objeto da classe Swallowable, desde que seu tamanho seja maior que o do objeto.
Ao engolir um objeto, o Hole deve crescer 10% do tamanho do objeto engolido. Por exemplo, se o Hole engolir uma Tree de tamanho 30, ele deve crescer 3 unidades.
size += item.getObjectSize() * 0.1; // Cresce 10% do tamanho do objeto engolido
}4. Ajuste a movimentação da Hole
Para melhorar a experiência do jogador, ajuste a movimentação da Hole para que seja pelo teclado. Utilize as teclas de seta para mover a Hole para cima, baixo, esquerda e direita. Isso tornará o controle do jogo mais intuitivo.
O Hole deve ficar no centro da tela quando o jogo começar e permanecer assim durante todo o jogo. Ao mover a Hole, o cenário (mundo) deve se mover em torno dele, criando a ilusão de que a Hole está se movendo pelo ambiente.
Para isso, você pode implementar a movimentação do cenário em resposta às teclas de seta pressionadas, ajustando a posição dos objetos no mundo em vez da Hole em si.
Então o código do método followMouse() da classe Hole não faz mais sentido.
Os objetos que podem ser engolidos pelo Hole devem adicionar a movimentação pelo cenário em seu método act(), verificando as teclas de seta pressionadas e ajustando a posição dos objetos em conformidade. Em qual classe esse código deve ficar?
//...
if (Greenfoot.isKeyDown("up")) {
// Move o objeto para baixo
}
if (Greenfoot.isKeyDown("down")) {
// Move o objeto para cima
}
if (Greenfoot.isKeyDown("left")) {
// Move o objeto para a direita
}
if (Greenfoot.isKeyDown("right")) {
// Move o objeto para a esquerda
}
//...Para que o objetos possam sair da tela (World), você pode passar um novo parâmetro no construtor da classe MyWorld (bounded) como false. Assim, o mundo não limitará os objetos dentro da tela.
import greenfoot.*;
public class MyWorld extends World {
public MyWorld() {
super(800, 800, 1, false);
}
}5. Exibindo pontuação
A pontuação (tamanho do Hole) deve ser exibida no canto superior esquerdo da tela.
Utilize o método showText da classe World para exibir a pontuação. Atualize a pontuação sempre que o Hole crescer.
// No método act() da classe Hole
// Exibe a pontuação no canto superior esquerdo
getWorld().showText("Pontos: " + size, 50, 25);Resultado Final
Ao final, seu código estará mais organizado, cada classe terá responsabilidades mais claras e adicionar novos objetos engolíveis (como um Carro ou uma Casa) será uma tarefa muito mais simples. Esta estrutura é um excelente exemplo do poder da herança e do encapsulamento na Orientação a Objetos.
Polimorfismo
- É a possibilidade de se solicitar um serviço a um objeto, cuja execução vai depender do tipo de objeto instanciado
Círculo,RetanguloeQuadradosão do tipoFigura.- Método desenhar()
O resultado depende do tipo de figura que receber a mensagem
O polimorfismo permite escrever programas que processam objetos que compartilham a mesma superclasse em uma hierarquia de classe como se todas fossem objetos da superclasse.
Sistema de simulação de movimento de Animais
- Peixes, Anfíbios, Pássaros
- Superclasse Animal
- Método mover
- Localização x,y
- Todas as subclasses implementam o método mover
- Superclasse Animal
- O programa envia a mensagem "mover" para os 3 objetos
//...
Animal animais[] = new Animal[3];
//...
for(int i = 0; i < 3 ; i++){
Animal animal = animais[i];
animal.mover();//como será o movimento desse animal?
}
//...List<Animal> animais = List.of(new Peixe(), new Anfibio(), new Passaro());
for(Animal animal : animais ) {
animal.mover();//como será o movimento desse animal?
}
- Cada animal responde ao método mover de uma maneira diferente
- O peixe pode nadar 2 metros
- Anfíbio pular 1 metro
- Pássaro voar 3 metros
- Cada objeto irá responder a mensagem "mover" de acordo com sua instancia
- Apesar de todos serem Animais o fato do método "mover" ter "muitas formas" é a chave do polimorfismo
- Polimorfismo vem de Polimorfo, "Que é sujeito a mudar de forma"
Definição
Polimorfismo possibilita tratar objetos de tipos mais especializados de forma genérica
Caelum
O que guarda uma variável do tipo Funcionario? Uma referência para um Funcionario, nunca o objeto em si.
Na herança, vimos que todo Gerente é um Funcionario, pois é uma extensão deste. Podemos nos referir a um Gerente como sendo um Funcionario. Se alguém precisa falar com um Funcionario do banco, pode falar com um Gerente! Porque? Pois Gerente é um Funcionario. Essa é a semântica da herança.
Gerente gerente = new Gerente();
Funcionario funcionario = gerente;
funcionario.setSalario(5000.0);Polimorfismo é a capacidade de um objeto poder ser referenciado de várias formas. (cuidado, polimorfismo não quer dizer que o objeto fica se transformando, muito pelo contrário, um objeto nasce de um tipo e morre daquele tipo, o que pode mudar é a maneira como nos referimos a ele).
Até aqui tudo bem, mas e se eu tentar:
funcionario.getBonificacao();Qual é o retorno desse método? 500 ou 750? No Java, a invocação de método sempre vai ser decidida em tempo de execução. O Java vai procurar o objeto na memória e, aí sim, decidir qual método deve ser chamado, sempre relacionando com sua classe de verdade, e não com a que estamos usando para referenciá-lo. Apesar de estarmos nos referenciando a esse Gerente como sendo um Funcionario, o método executado é o do Gerente. O retorno é 750.
Parece estranho criar um gerente e referenciá-lo como apenas um funcionário. Por que faríamos isso? Na verdade, a situação que costuma aparecer é a que temos um método que recebe um argumento do tipo Funcionario:
class ControleDeBonificacoes {
private double totalDeBonificacoes = 0;
public void registra(Funcionario funcionario) {
this.totalDeBonificacoes += funcionario.getBonificacao();
}
public double getTotalDeBonificacoes() {
return this.totalDeBonificacoes;
}
}E, em algum lugar da minha aplicação (ou no main, se for apenas para testes):
ControleDeBonificacoes controle = new ControleDeBonificacoes();
Gerente funcionario1 = new Gerente();
funcionario1.setSalario(5000.0);
controle.registra(funcionario1);
Funcionario funcionario2 = new Funcionario();
funcionario2.setSalario(1000.0);
controle.registra(funcionario2);
IO.println(controle.getTotalDeBonificacoes());Perceba que conseguimos passar um Gerente para um método que recebe um Funcionario como argumento. Pense como numa porta na agência bancária com o seguinte aviso: "Permitida a entrada apenas de Funcionários". Um gerente pode passar nessa porta? Sim, pois Gerente é um Funcionario.
Qual será o valor resultante? Não importa que dentro do método registra do ControleDeBonificacoes receba Funcionario. Quando ele receber um objeto que realmente é um Gerente, o seu método reescrito será invocado. Reafirmando: não importa como nos referenciamos a um objeto, o método que será invocado é sempre o que é dele.
No dia em que criarmos uma classe Secretaria, por exemplo, que é filha de Funcionario, precisaremos mudar a classe de ControleDeBonificacoes? Não. Basta a classe Secretaria reescrever os métodos que lhe parecerem necessários. É exatamente esse o poder do polimorfismo, juntamente com a reescrita de método: diminuir o acoplamento entre as classes, para evitar que novos códigos resultem em modificações em inúmeros lugares.
Perceba que quem criou ControleDeBonificacoes pode nunca ter imaginado a criação da classe Secretaria ou Engenheiro. Contudo, não será necessário reimplementar esse controle em cada nova classe: reaproveitamos aquele código
Herança versus acoplamento
Note que o uso de herança aumenta o acoplamento entre as classes, isto é, o quanto uma classe depende de outra. A relação entre classe mãe e filha é muito forte e isso acaba fazendo com que o programador das classes filhas "tenha que conhecer a implementação da classe pai e vice-versa". Fica difícil fazer uma mudança pontual no sistema.
Por exemplo, imagine se tivermos que mudar algo na nossa classe Funcionario, mas não quiséssemos que todos os funcionários sofressem a mesma mudança. Precisaríamos passar por cada uma das filhas de Funcionario verificando se ela se comporta como deveria ou se devemos sobrescrever o tal método modificado.
Esse é um problema da herança, e não do polimorfismo, que resolveremos mais tarde com a ajuda de Interfaces.
Um outro exemplo
Imagine que vamos modelar um sistema para a faculdade que controle as despesas com funcionários e professores. Nosso funcionário fica assim:
class EmpregadoDaFaculdade {
private String nome;
private double salario;
double getGastos() {
return this.salario;
}
String getInfo() {
return "nome: " + this.nome + " com salário " + this.salario;
}
// métodos de get, set e outros
}O gasto que temos com o professor não é apenas seu salário. Temos de somar um bônus de 10 reais por hora/aula. O que fazemos então? Reescrevemos o método. Assim como o getGastos é diferente, o getInfo também será, pois temos de mostrar as horas/aula também.
class ProfessorDaFaculdade extends EmpregadoDaFaculdade {
private int horasDeAula;
double getGastos() {
return this.getSalario() + this.horasDeAula * 10;
}
String getInfo() {
String informacaoBasica = super.getInfo();
String informacao = informacaoBasica + " horas de aula: " + this.horasDeAula;
return informacao;
}
// métodos de get, set e outros que forem necessários
}A novidade, aqui, é a palavra chave super. Apesar do método ter sido reescrito, gostaríamos de acessar o método da classe mãe, para não ter de copiar e colocar o conteúdo desse método e depois concatenar com a informação das horas de aula.
Como tiramos proveito do polimorfismo? Imagine que temos uma classe de relatório:
class GeradorDeRelatorio {
public void adiciona(EmpregadoDaFaculdade f) {
IO.println(f.getInfo());
IO.println(f.getGastos());
}
}Podemos passar para nossa classe qualquer EmpregadoDaFaculdade! Vai funcionar tanto para professor, quanto para outros funcionários.
Um certo dia, muito depois de terminar essa classe de relatório, resolvemos aumentar nosso sistema, e colocar uma classe nova, que representa o Reitor. Como ele também é um EmpregadoDaFaculdade, será que vamos precisar alterar algo na nossa classe de Relatorio? Não. Essa é a ideia! Quem programou a classe GeradorDeRelatorio nunca imaginou que existiria uma classe Reitor e, mesmo assim, o sistema funciona.
class Reitor extends EmpregadoDaFaculdade {
// informações extras
String getInfo() {
return super.getInfo() + " e ele é um reitor";
}
// não sobrescrevemos o getGastos!!!
}K19
Controle de Ponto
O sistema do banco deve possuir um controle de ponto para registrar a entrada e saída dos funcionários. O pagamento dos funcionários depende dessas informações. Podemos definir uma classe para implementar o funcionamento de um relógio de ponto.
class ControleDePonto {
private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
public void registraEntrada(Gerente g) {
LocalDateTime agora = LocalDateTime.now();
IO.println("ENTRADA:"+g.getCodigo());
IO.println("DATA:"+agora.format(dtf);
}
public void registraSaida(Gerente g) {
LocalDateTime agora = LocalDateTime.now();
IO.println("SAÍDA:"+g.getCodigo());
IO.println("DATA:"+agora.format(dtf);
}
}A classe acima possui dois métodos: o primeiro para registrar a entrada e o segundo para registrar a saída dos gerentes do banco. Contudo, esses dois métodos não são aplicáveis aos outros tipos de funcionários.
Seguindo essa abordagem, a classe ControleDePonto precisaria de um par de métodos para cada cargo. Então, a quantidade de métodos dessa classe seria igual a quantidade de cargos multiplicada por dois. Imagine que no banco exista 30 cargos distintos. Teríamos 60 métodos na classe ControleDePonto.
Os procedimentos de registro de entrada e saída são idênticos para todos os funcionários. Consequentemente, qualquer alteração na lógica desses procedimentos implicaria na modificação de todos os métodos da classe ControleDePonto.
Além disso, se o banco definir um novo tipo de funcionário, dois novos métodos praticamente
idênticos aos que já existem teriam de ser adicionados na classe ControleDePonto. Analogamente, se um cargo deixar de existir, os dois métodos correspondentes da classe ControleDePonto deverão ser retirados.
Modelagem dos funcionários
Com o intuito inicial de reutilizar código, podemos modelar os diversos tipos de funcionários do banco utilizando o conceito de herança.
class Funcionario {
private int codigo ;
// GETTERS AND SETTERS
}class Gerente extends Funcionario {
private String usuario ;
private String senha ;
// GETTERS AND SETTERS
}class Telefonista extends Funcionario {
private int ramal ;
// GETTERS AND SETTERS
}É UM (extends)
Além de gerar reaproveitamento de código, a utilização de herança permite que objetos criados a partir das classes específicas sejam tratados como objetos da classe genérica.
Em outras palavras, a herança entre as classes que modelam os funcionários permite que objetos criados a partir das classes Gerente ou Telefonista sejam tratados como objetos da classe Funcionario.
No código da classe Gerente utilizamos a palavra extends. Ela pode ser interpretada como a
expressão: É UM ou É UMA.
class Gerente extends Funcionario{
// TODO Gerente É UM Funcionario
}Como está explícito no código que todo gerente é um funcionário então podemos criar um objeto da classe Gerente e tratá-lo como um objeto da classe Funcionario também.
// Criando um objeto da classe Gerente
Gerente g = new Gerente();
// Tratando um gerente como um objeto da classe Funcionario
Funcionario f = g ;Em alguns lugares do sistema do banco será mais vantajoso tratar um objeto da classe Gerente como um objeto da classe Funcionario.
Melhorando o controle de ponto
O registro da entrada ou saída não depende do cargo do funcionário. Não faz sentido criar um método que registre a entrada para cada tipo de funcionário, pois eles serão sempre idênticos. Analogamente, não faz sentido criar um método que registre a saída para cada tipo de funcionário.
Dado que podemos tratar os objetos das classes derivadas de Funcionario como sendo objetos dessa classe, podemos implementar um método que seja capaz de registrar a entrada de qualquer funcionário independentemente do cargo. Analogamente, podemos fazer o mesmo para o procedimento de saída.
class ControleDePonto {
public void registraEntrada(Funcionario f) {
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/ yyyy HH:mm:ss") ;
Date agora = new Date() ;
System.out.println("ENTRADA:"+f.getCodigo());
System.out.println("DATA:"+sdf.format(agora));
}
public void registraSaida(Funcionario f) {
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/ yyyy HH:mm:ss") ;
Date agora = new Date() ;
System.out.println("SAÍDA:"+f.getCodigo());
System.out.println("DATA:"+sdf.format(agora));
}
}Os métodos registraEntrada() e registraSaida() recebem referências de objetos da classe Funcionario como parâmetro. Consequentemente, podem receber referências de objetos de qualquer classe que deriva direta ou indiretamente da classe Funcionario.
A capacidade de tratar objetos criados a partir das classes específicas como objetos de uma classe genérica é chamada de polimorfismo.
Aplicando a ideia do polimorfismo no controle de ponto, facilitamos a manutenção da classe ControleDePonto. Qualquer alteração no procedimento de entrada ou saída implica em alterações em métodos únicos.
Além disso, novos tipos de funcionários podem ser definidos sem a necessidade de qualquer alteração na classe ControleDePonto. Analogamente, se algum cargo deixar de existir, nada precisará ser modificado na classe ControleDePonto.
Referências
Classes Abstratas e Interface
- Classes abstratas são classes que não produzem instâncias. Elas agrupam características e comportamentos que serão herdados por outras classes
- Fornecem padrões de comportamento que serão implementados nas suas subclasses
- Podem ter métodos com implementação definida
- Não pode ser instanciada diretamente (
new). - Uma classe abstrata possui características que devem/podem ser implementadas por classes filhas
- Os métodos abstratos são obrigatoriamente implementados pelas classes filhas concretas, quando a mesma herda de uma classe abstrata.
public abstract class Pessoa {
int matricula;
String nome;
public abstract void estacionar();
public void entrar(){
System.out.println("Entrando na Faculdade");
}
}public class Aluno extends Pessoa {
double media;
public void estacionar(){
System.out.println("Estacionando na área para estudante...");
}
}public class Professor extends Pessoa {
double salario;
public void estacionar(){
System.out.println("Estacionando nas vagas de professor");
}
}Outros Exemplos
Caelum
Vamos recordar em como pode estar nossa classe Funcionario:
class Funcionario {
protected String nome;
protected String cpf;
protected double salario;
public double getBonificacao() {
return this.salario * 1.2;
}
// outros métodos aqui
}Considere o nosso ControleDeBonificacao:
class ControleDeBonificacoes {
private double totalDeBonificacoes = 0;
public void registra(Funcionario f) {
System.out.println("Adicionando bonificação do funcionario: " + f);
this.totalDeBonificacoes += f.getBonificacao();
}
public double getTotalDeBonificacoes() {
return this.totalDeBonificacoes;
}
}Nosso método registra recebe qualquer referência do tipo Funcionario, isto é, podem ser objetos do tipo Funcionario e qualquer de seus subtipos: Gerente, Diretor e, eventualmente, alguma nova subclasse que venha ser escrita, sem prévio conhecimento do autor da ControleDeBonificacao.
Estamos utilizando aqui a classe Funcionario para o polimorfismo. Se não fosse ela, teríamos um grande prejuízo: precisaríamos criar um método registra para receber cada um dos tipos de Funcionario, um para Gerente, um para Diretor, etc. Repare que perder esse poder é muito pior do que a pequena vantagem que a herança traz em herdar código.
Porém, em alguns sistemas, como é o nosso caso, usamos uma classe com apenas esses intuitos: de economizar um pouco código e ganhar polimorsmo para criar métodos mais genéricos, que se encaixem a diversos objetos.
Faz sentido ter uma referência do tipo Funcionario? Essa pergunta é diferente de saber se faz sentido ter um objeto do tipo Funcionario: nesse caso, faz sim e é muito útil.
Referenciando Funcionario temos o polimorfismo de referência, já que podemos receber qualquer objeto que seja um Funcionario. Porém, dar new em Funcionario pode não fazer sentido, isto é, não queremos receber um objeto do tipo Funcionario, mas sim que aquela referência seja ou um Gerente, ou um Diretor, etc. Algo mais concreto que um Funcionario.
ControleDeBonificacoes cdb = new ControleDeBonificacoes();
Funcionario f = new Funcionario();
cdb.adiciona(f); // faz sentido?Vejamos um outro caso em que não faz sentido ter um objeto daquele tipo, apesar da classe existir: imagine a classe Pessoa e duas filhas, PessoaFisica e PessoaJuridica. Quando puxamos um relatório de nossos clientes (uma array de Pessoa por exemplo), queremos que cada um deles seja ou uma PessoaFisica, ou uma PessoaJuridica. A classe Pessoa, nesse caso, estaria sendo usada apenas para ganhar o polimorfismo e herdar algumas coisas: não faz sentido permitir instanciá-la.
Para resolver esses problemas, temos as classes abstratas.
Classe abstrata
O que, exatamente, vem a ser a nossa classe Funcionario? Nossa empresa tem apenas Diretores, Gerentes, Secretárias, etc. Ela é uma classe que apenas idealiza um tipo, define apenas um rascunho.
Para o nosso sistema, é inadmissível que um objeto seja apenas do tipo Funcionario (pode existir um sistema em que faça sentido ter objetos do tipo Funcionario ou apenas Pessoa, mas, no nosso caso, não).
Usamos a palavra chave abstract para impedir que ela possa ser instanciada. Esse é o efeito direto de se usar o modificador abstract na declaração de uma classe:
abstract class Funcionario {
protected double salario;
public double getBonificacao() {
return this.salario * 1.2;
}
// outros atributos e métodos comuns a todos Funcionarios
}E, no meio de um código:
Funcionario f = new Funcionario(); // não compila!!!O código acima não compila. O problema é instanciar a classe - criar referência, você pode. Se ela não pode ser instanciada, para que serve? Serve para o polimorfismo e herança dos atributos e métodos, que são recursos muito poderosos, como já vimos.
Vamos então herdar dessa classe, reescrevendo o método getBonificacao
class Gerente extends Funcionario {
public double getBonificacao() {
return this.salario * 1.4 + 1000;
}
}Mas qual é a real vantagem de uma classe abstrata? Poderíamos ter feito isto com uma herança comum. Por enquanto, a única diferença é que não podemos instanciar um objeto do tipo Funcionario, que já é de grande valia, dando mais consistência ao sistema.
Fique claro que a nossa decisão de transformar Funcionario em uma classe abstrata dependeu do nosso domínio. Pode ser que, em um sistema com classes similares, faça sentido que uma classe análoga a Funcionario seja concreta.
Métodos abstratos
Se o método getBonificacao não fosse reescrito, ele seria herdado da classe mãe, fazendo com que devolvesse o salário mais 20%.
Levando em consideração que cada funcionário em nosso sistema tem uma regra totalmente diferente para ser bonificado, faz algum sentido ter esse método na classe Funcionario? Será que existe uma bonificação padrão para todo tipo de Funcionario? Parece que não, cada classe filha terá um método diferente de bonificação pois, de acordo com nosso sistema, não existe uma regra geral: queremos que cada pessoa que escreve a classe de um Funcionario diferente (subclasses de Funcionario) reescreva o método getBonificacao de acordo com as suas regras.
Poderíamos, então, jogar fora esse método da classe Funcionario? O problema é que, se ele não existisse, não poderíamos chamar o método apenas com uma referência a um Funcionario, pois ninguém garante que essa referência aponta para um objeto que possui esse método. Será que então devemos retornar um código, como um número negativo? Isso não resolve o problema: se esquecermos de reescrever esse método, teremos dados errados sendo utilizados como bônus.
Existe um recurso em Java que, em uma classe abstrata, podemos escrever que determinado método será sempre escrito pelas classes filhas. Isto é, um método abstrato.
Ele indica que todas as classes filhas (concretas, isto é, que não forem abstratas) devem reescrever esse método ou não compilarão. É como se você herdasse a responsabilidade de ter aquele método.
Como declarar um método abstrato
Às vezes, não fica claro como declarar um método abstrato.
Basta escrever a palavra chave abstract na assinatura do mesmo e colocar um ponto e vírgula em vez de abre e fecha chaves!
abstract class Funcionario {
abstract double getBonificacao();
// outros atributos e métodos
}K19
Classes Abstratas
No banco, todas as contas são de um tipo específico. Por exemplo, conta poupança, conta corrente ou conta salário. Essas contas poderiam ser modeladas através das seguintes classes utilizando o conceito de herança:
class Conta {
// Atributos
// Construtores
// Métodos
}class ContaPoupanca extends Conta {
// Atributos
// Construtores
// Métodos
}class ContaCorrente extends Conta {
// Atributos
// Construtores
// Métodos
}Para cada conta do domínio do banco devemos criar um objeto da classe correspondente ao tipo da conta. Por exemplo, se existe uma conta poupança no domínio do banco devemos criar um objeto da classe ContaPoupanca.
ContaPoupanca cp = new ContaPoupanca();Faz sentido criar objetos da classe ContaPoupanca pois existem contas poupança no domínio do banco. Dizemos que a classe ContaPoupanca é uma classe concreta pois criaremos objetos a partir dela.
Por outro lado, a classe Conta não define uma conta que de fato existe no domínio do banco. Ela apenas serve como "base" para definir as contas concretos.
Não faz sentido criar um objeto da classe Conta pois estaríamos instanciado um objeto que não é suficiente para representar uma conta que pertença ao domínio do banco. Mas, a princípio, não há nada proibindo a criação de objetos dessa classe. Para adicionar essa restrição no sistema, devemos tornar a classe Conta abstrata.
Uma classe concreta pode ser diretamente utilizada para instanciar objetos. Por outro lado, uma classe abstrata não pode. Para definir uma classe abstrata, basta adicionar o modificador abstract.
abstract class Conta {
// Atributos
// Construtores
// Métodos
}Todo código que tenta criar um objeto de uma classe abstrata não compila.
// Erro de compilação
Conta c = new Conta();Métodos Abstratos
Suponha que o banco ofereça extrato detalhado das contas e para cada tipo de conta as informações e o formato desse extrato detalhado são diferentes. Além disso, a qualquer momento o banco pode mudar os dados e o formato do extrato detalhado de um dos tipos de conta.
Neste caso, parece não fazer sentido ter um método na classe Conta para gerar extratos detalhados pois ele seria reescrito nas classes específicas sem nem ser reaproveitado.
Poderíamos, simplesmente, não definir nenhum método para gerar extratos detalhados na classe Conta. Porém, não haveria nenhuma garantia que as classes que derivam direta ou indiretamente da classe Conta implementem métodos para gerar extratos detalhados.
Mas, mesmo supondo que toda classe derivada implemente um método para gerar os extratos que desejamos, ainda não haveria nenhuma garantia em relação as assinaturas desses métodos. As classes derivadas poderiam definir métodos com nomes ou parâmetros diferentes. Isso prejudicaria a utilização dos objetos que representam as contas devido a falta de padronização das operações.
Para garantir que toda classe concreta que deriva direta ou indiretamente da classe Conta tenha uma implementação de método para gerar extratos detalhados e além disso que uma mesma assinatura de método seja utilizada, devemos utilizar o conceito de métodos abstratos.
Na classe Conta, definimos um método abstrato para gerar extratos detalhados. Um método abstrato não possui corpo (implementação).
abstract class Conta {
// Atributos
// Construtores
// Métodos
public abstract void imprimeExtratoDetalhado();
}As classes concretas que derivam direta ou indiretamente da classe Conta devem possuir uma implementação para o método imprimeExtratoDetalhado().
class ContaPoupanca extends Conta {
private int diaDoAniversario ;
public void imprimeExtratoDetalhado(){
System.out.println("EXTRATO DETALHADO DE CONTA POUPANÇA") ;
SimpleDateFormat sdf = new SimpleDateFormat ("dd/MM/yyyy HH:mm:ss") ;
Date agora = new Date();
System.out.println("DATA:"+sdf.format(agora));
System.out.println("SALDO:"+this.getSaldo());
System.out.println("ANIVERSÁRIO:"+this.diaDoAniversario);
}
}Se uma classe concreta derivada da classe Conta não possuir uma implementação do método imprimeExtratoDetalhado() ela não compilará.
// ESSA CLASSE NÃO COMPILA
class ContaPoupanca extends Conta {
}Interface
Padronização
No dia a dia, estamos acostumados a utilizar aparelhos que dependem de energia elétrica. Esses aparelhos possuem um plugue que deve ser conectado a uma tomada para obter a energia necessária.
Diversas empresas fabricam aparelhos elétricos com plugues. Analogamente, diversas empresas fabricam tomadas elétricas. Suponha que cada empresa decida por conta própria o formato dos plugues ou das tomadas que fabricará. Teríamos uma infinidade de tipos de plugues e tomadas que tornaria a utilização dos aparelhos elétricos uma experiência extremamente desagradável.
Inclusive, essa falta de padrão pode gerar problemas de segurança aos usuários. Os formatos dos plugues ou das tomadas pode aumentar o risco de uma pessoa tomar um choque elétrico.
Com o intuito de facilitar a utilização dos consumidores e aumentar a segurança dos mesmos, o governo através dos órgãos responsáveis estabelece padrões para os plugues e tomadas. Esses padrões estabelecem restrições que devem ser respeitadas pelos fabricantes dos aparelhos e das tomadas.
Em diversos contextos, padronizar pode trazer grandes benefícios. Inclusive, no desenvolvimento de aplicações. Mostraremos como a ideia de padronização pode ser contextualizada nos conceitos de orientação a objetos.
Contratos
Num sistema orientado a objetos, os objetos interagem entre si através de chamadas de métodos (troca de mensagens). Podemos dizer que os objetos se “encaixam” através dos métodos públicos assim como um plugue se encaixa em uma tomada através dos pinos.
Para os objetos de uma aplicação “conversarem” entre si mais facilmente é importante padronizar o conjunto de métodos oferecidos por eles. Assim como os plugues encaixam nas tomadas mais facilmente graças aos padrões definidos pelo governo.
Um padrão é definido através de especificações ou contratos. Nas aplicações orientadas a objetos, podemos criar um “contrato” para definir um determinado conjunto de métodos que deve ser implementado pelas classes que “assinarem” este contrato. Em orientação a objetos, um contrato é chamado de interface. Um interface é composta basicamente por métodos abstratos.
Exemplo
No sistema do banco, podemos definir uma interface (contrato) para padronizar as assinaturas dos métodos oferecidos pelos objetos que representam as contas do banco.
interface Conta {
void deposita ( double valor ) ;
void saca ( double valor ) ;
}Os métodos de uma interface não possuem corpo (implementação) pois serão implementados nas classes vinculadas a essa interface. Todos os métodos de uma interface devem ser públicos e abstratos. Os modificadores public e abstract são opcionais.
As classes que definem os diversos tipos de contas que existem no banco devem implementar (assinar) a interface Conta.
class ContaPoupanca implements Conta {
public void deposita ( double valor ) {
// implementacao
}
public void saca ( double valor ) {
// implementacao
}
}class ContaCorrente implements Conta {
public void deposita ( double valor ) {
// implementacao
}
public void saca ( double valor ) {
// implementacao
}
}As classes concretas que implementam uma interface são obrigadas a possuir uma implementação para cada método declarado na interface. Caso contrário, ocorrerá um erro de compilação.
// Esta classe não compila porque ela não implementou o método saca ()
class ContaCorrente implements Conta {
public void deposita ( double valor ) {
// implementacao
}
}A primeira vantagem de utilizar uma interface é a padronização das assinaturas dos métodos oferecidos por um determinado conjunto de classes. A segunda vantagem é garantir que determinadas classes implementem certos métodos.
Se comporta como um
A relação entre uma interface e uma classe que a implementa é semelhante a relação de herança entre classes. Porem, nessa herança, não tem o que ser herdado, apenas vem a obrigação de implementar os métodos declarados na interface.
Nessa relação, podemos dizer que uma classe que implementa uma interface "se comporta como" a interface que ela implementa.
Por exemplo, em um sistema bancário, podemos ter um produto que seja uma PrevidenciaPrivada. Esse produto pode ser tratado como uma conta bancária para fins de depósito e saque de dinheiro.
class PrevidenciaPrivada implements Conta {
public void deposita ( double valor ) {
// implementacao
}
public void saca ( double valor ) {
// implementacao
}
}Dessa forma, podemos dizer que a classe PrevidenciaPrivada se comporta como uma Conta pois ela implementa a interface Conta.
Polimorfismo
Se uma classe implementa uma interface, podemos aplicar a ideia do polimorfismo assim como quando aplicamos herança. Dessa forma, outra vantagem da utilização de interfaces é o ganho do polimorfismo.
Como exemplo, suponha que a classe ContaCorrente implemente a interface Conta. Podemos guardar a referência de um objeto do tipo ContaCorrente em uma variável do tipo Conta.
Conta c = new ContaCorrente();Além disso, podemos passar uma variável do tipo ContaCorrente para um método que o parâmetro seja do tipo Conta.
class GeradorDeExtrato {
public void geraExtrato ( Conta c ) {
// implementação
}
}GeradorDeExtrato g = new GeradorDeExtrato();
ContaCorrente c = new ContaCorrente();
g.geraExtrato(c) ;O método geraExtrato() pode ser utilizado para objetos criados a partir de classes que implementam direta ou indiretamente a interface Conta.
Interface e Herança
As vantagens e desvantagens entre interface e herança, provavelmente, é um dos temas mais discutido nos blogs, fóruns e revistas que abordam desenvolvimento de software orientado a objetos.
Muitas vezes, os debates sobre este assunto se estendem mais do que a própria importância desse tópico. Muitas pessoas se posicionam de forma radical defendendo a utilização de interfaces ao invés de herança em qualquer situação.
Normalmente, esses debates são direcionados na análise do que é melhor para manutenção das aplicações: utilizar interfaces ou aplicar herança.
A grosso modo, priorizar a utilização de interfaces permite que alterações pontuais em determinados trechos do código fonte sejam feitas mais facilmente pois diminui as ocorrências de efeitos colaterais indesejados no resto da aplicação. Por outro lado, priorizar a utilização de herança pode diminuir a quantidade de código escrito no início do desenvolvimento de um projeto.
Algumas pessoas propõem a utilização de interfaces juntamente com composição para substituir totalmente o uso de herança. De fato, esta é uma alternativa interessante pois possibilita que um trecho do código fonte de uma aplicação possa ser alterado sem causar efeito colateral no restante do sistema além de permitir a reutilização de código mais facilmente.
Em Java, como não há herança múltipla, muitas vezes, interfaces são apresentadas como uma alternativa para obter um grau maior de polimorfismo.
Por exemplo, suponha duas árvores de herança independentes
Suponha que os gerentes e as empresas possam acessar o sistema do banco com um nome de usuário e uma senha. Seria interessante utilizar um único método para implementar a autenticação desses dois tipos de objetos. Mas, qual seria o tipo de parâmetro deste método? Lembrando que ele deve aceitar gerentes e empresas.
class AutenticadorDeUsuario {
public boolean autentica (??? u ) {
// implementação
}
}De acordo com as árvores de herança, não há polimorfismo entre objetos da classe Gerente e da classe Empresa. Para obter polimorfismo entre os objetos dessas duas classes somente com herança, deveríamos colocá-las na mesma árvore de herança. Mas, isso não faz sentido pois uma empresa não é um funcionário e o gerente não é cliente. Neste caso, a solução é utilizar interfaces para obter o polimorfismo desejado
Agora, conseguimos definir o que o método autentica() deve receber como parâmetro para trabalhar tanto com gerentes quanto com empresas. Ele deve receber um parâmetro do tipo Usuario.
public interface Usuario {
boolean autenticar();
}public class Cliente {
//...
}public class PessoaFisica extends Cliente {
//...
}public class PessoaJuridica extends Cliente implements Usuario {
//...
public boolean autenticar(){
return true;
}
}public class Funcionario {
//...
}public class Gerente extends Funcionario implements Usuario {
//...
public boolean autenticar(){
return true;
}
}public class Seguranca extends Funcionario {
//...
}public class AutenticadorDeUsuario {
public boolean autentica ( Usuario u ) {
// implementação
}
}Mais sobre herança e interface
public interface Conta {
double getSaldo();
void deposita(double valor);
void saca(double valor);
void atualiza(double taxaSelic);
}
class ContaCorrente implements Conta {
// ...
}
class ContaPoupanca implements Conta {
// ...
}Às vezes, é interessante criarmos uma interface que herda de outras interfaces: essas, são chamadas subinterfaces. Essas, nada mais são do que um agrupamento de obrigações para a classe que a implementar
interface Tributavel {
//...
public void calcularTributo();
}interface ContaTributavel extends Conta, Tributavel {
}Dessa maneira, quem for implementar essa nova interface precisa implementar todos os métodos herdados das suas superinterfaces (e talvez ainda novos métodos declarados dentro dela):
class ContaInvestimento implements ContaTributavel {
// métodos
}ContaTributavel ct = new ContaInvestimento();
Conta c = new ContaInvestimento();
Tributavel t = new ContaInvestimento();Perceba que o código pode parecer estranho, pois a interface não declara método algum, só herda os métodos abstratos declarados nas outras interfaces. Ao mesmo tempo que uma interface pode herdar de mais de uma outra interface, classes só podem possuir uma classe mãe (herança simples).
Referências
Exceptions
Considerando o que foi visto em Pilha de execução.
Quando um exceção (situação excepcional) ocorre, o JVM entra em estado de alerta e procura dento do metodo se existe algum tratamento especial para o problema.
class TesteErro {
public static void main(String[] args) {
IO.println("inicio do main");
try {
metodo1();
} catch (ArrayIndexOutOfBoundsException e) {
IO.println("deu erro: ");
}
IO.println("fim do main");
}
static void metodo1() {
IO.println("inicio do metodo1");
metodo2();
IO.println("fim do metodo1");
}
static void metodo2() {
IO.println("inicio do metodo2");
int[] array = new int[10];
for (int i = 0; i <= 15; i++) {
array[i] = i;
IO.println(i);
}
IO.println("fim do metodo2");
}
}- Como o
metodo2não tem nenhum tratamento a JVM interrompe sua execução e volta um nível na pilha e verifica novamente. - Como o
metodo1também não faz nenhum tratamento a JVM sobe mais um nivel até chegar nomain - Como o
maintambém não faz nenhum tratamento a Thread morre.
O tratamento de erros em Java é feito em tempo de execução através do tratamento de exceção. As exceções são classes que seguem o modelo OO e são lançadas quando o sistema encontra um problema mas podem ser utilizadas também para validar regras de negócio.
Exception(exceção) significa "condição excepcional", e é uma ocorrência que altera o fluxo normal do programa.
Tips
Quando um evento excepcional ocorre em java, diz-se que uma exceção será lançada.
- Métodos podem capturar ou deixar passar exceções que ocorrerem em seu corpo, mas para isto é obrigatório que o método declare a sua decisão.
- Para repassar o tratamento de erro para quem chama o método utilizamos o
throws.throwsdeclara que o método pode provocar exceções do tipo declarado (ou de qualquer subtipo).
public void validar() throws Excecao1, Excecao2 {…}Para tratar a exceção no método utilizamos o try/catch.
try {
for (int i = 0; i <= 15; i++) {
array[i] = i;
IO.println(i);
}
} catch (ArrayIndexOutOfBoundsException e) {
IO.println("erro: " + e);
}Executando o código novamente
erro: java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 10
fim do metodo2
fim do metodo1
fim do main?
- Modificando o try para dentro do for qual será o comportamento?
- E na chamada do metodo2?
- E na chamada do metodo1?
- Divisão por 0
- Referência Nula
A partir do momento que uma exception foi catched (pega, tratada, capturada , handled), a execução volta ao normal a partir daquele ponto.
ArrayIndexOutOfBoundsException ou um NullPointerException poderia ser facilmente evitado com o for corretamente escrito ou com ifs que checariam os limites da array. Tais problemas provavelmente poderiam ser evitados pelo programador
Tipos de Exception
- Checadas (Verificadas) –> o compilador verifica e obriga os usuários que chamam o método ou construtor a tratar a exceção
- Não – checadas –> o compilador não verifica, são os subtipos de Error e RuntimeException
Tips
RuntimeException é a exception mãe de todas as exceptions não checadas
Abrir um arquivo para leitura
public class AbrirArquivo {
public static void metodo() {
new java.io.FileInputStream("arquivo.txt");
}
}O código acima não compila e o compilador avisa que é necessário tratar o FileNotFoundException que pode ocorrer.Para compilar e fazer o programa funcionar, temos duas maneiras que podemos tratar o problema. A primeira é tratá-lo com o try e catch e a segunda forma de tratar esse erro, é delegar ele para quem chamou o nosso método, isto é, passar para a frente.
public static void metodo() {
try {
new java.io.FileInputStream("arquivo.txt");
} catch (java.io.FileNotFoundException e) {
IO.println("Não foi possível encontrar o arquivo para leitura");
}
}public static void metodo() throws java.io.FileNotFoundException {
new java.io.FileInputStream("arquivo.txt");
}É possível fazer o tratamento de mais de uma exceção no mesmo bloco para ambas abordagens
try{
//Codigo verificado
}catch(TipoExcecao1 ex1){
//Captura uma exceção TipoExcecao1
}catch(TipoExcecao2 ex2){
//Captura uma exceção TipoExcecao2
}public void metodo() throws TipoExcecao1, TipoExcecao2 {
//…
}package exceptions.connection;
import java.sql.Connection;
import java.sql.SQLException;
public class DAO {
Connection connection;
public DAO() {
this.connection = new ConnectionFactory().getConnection();
}
public void save(String sql) {
try {
this.connection.prepareStatement(sql).execute();
connection.close();
} catch (SQLException e) {
// mandar mensagem para equipe de desenvolvimento
e.printStackTrace();
}
catch (Exception e) {
// mandar mensagem para equipe de infra
e.printStackTrace();
}
}
}Não há uma regra para decidir em que momento do seu programa deve ser feito o tratamento da exceção. Essa decisão depende de como a exceção será tratada e em que ponto é possivel fazer algo a respeito. Enquanto não for o momento, provavelmente será melhor delegar a responsabilidade para o método que invocou. Lembrando que: caso o tratamento não seja feito por nenhum código quem irá tratar é a JVM.
try {
for(int i = 0; i <= 15; i++) {
array[i] = i;
IO.println(i);
}
} catch (ArrayIndexOutOfBoundsException e) {
IO.println("erro: " + e);
}for(int i = 0; i <= 15; i++) {
try {
array[i] = i;
IO.println(i);
} catch (ArrayIndexOutOfBoundsException e) {
IO.println("erro: " + e);
}
}Para lançar a Exceção explicitamente utilizamos o throw e criamos uma instancia da classe que representa a exceção desejada
public class MinhaException extends Exception {
}
...{
public double dividir(double v1, double v2) throws MinhaException {
if(v2==0){
throw new MinhaException("Divisão por ZERO");
}
}
}Finally
Os blocos try e catch podem conter uma terceira cláusula chamada finally que indica o que deve ser feito após o término do bloco try ou de um catch.
try {
// bloco try
} catch (IOException ex) {
// bloco catch 1
} catch (SQLException sqlex) {
// bloco catch2
} finally {
// bloco finally
}É interessante colocar algo que é imprescindível de ser executado, caso o que você queria fazer tenha dado certo, ou não. O caso mais comum é o de liberar um recurso no finally, como um arquivo ou conexão com banco de dados, para que possamos ter a certeza de que aquele arquivo (ou conexão) vá ser fechado, mesmo que algo tenha falhado no decorrer do código.
O bloco finally sempre será executado, salvo em raras situações.
De forma geral ele é a garantia de que seu código irá liberar recursos ocupados mesmo que ocorram exceções (Exceptions) ou o método contendo o try retorne prematuramente (return).
Tips
Os únicos momentos em que o finally não será chamado são:
- Se você chamar System.exit() ou
- um outro thread interromper o atual (através do método interrupt()) ou
- Se a JVM der crash antes.
- O bloco
trydeve ser precedido por umcatchoufinalliy - O
finallyquer dizer que dando erro ou não o trecho de código compreendido nele será executado - O
catchserá executa somente se naquele trecho dentro do try resultar em algum erro
Tips
RuntimeException é a exception mãe de todas as exceptions não verificadas
Tips
IllegalArgumentException é uma exceção do pacote do java que podemos utilizar para tratar valores indevidos para chamadas de métodos
Exercício
Links w3schools
- Exception handling
- try and catch blocks
- Multiple catch blocks
- Nested try block
- Finally
- throw
- throws
- Exception propagation
- Exception handling with method overriding
- Custom exception
- Throwable class
Jackson Pires .O que é Programação Orientada a Objetos e porque você precisa saber! https://becode.com.br/programacao-orientada-a-objetos-poo/. (Acessado em 15/08/2019) ↩︎
Kennedy Tedesco. Linguagens e paradigmas de programação - Blog da TreinaWeb. https://www.treinaweb.com.br/blog/linguagens-e-paradigmas-de-programacao/. (Acessado em 07/08/2019). ↩︎
P.D. DEITEL e H. Deitel.JAVA: como programar, 10a Edição.Pearson, 2016. ↩︎
Nemora Dornelles.As 15 principais linguagens de programação do mundo! | Becode. https://becode.com.br/principais-linguagens-de-programacao/. (Acessado em 15/08/2019). ↩︎
Oracle."Hello World!" for Microsoft Windows (The Java™Tutorials > Getting Started> The "Hello World!" Application). https://docs.oracle.com/javase/tutorial/getStarted/cupojava/win32.html. (Acessado em 07/08/2019). ↩︎
R. Santos.Introdução à programação orientada a objetos usando Java. Campus, 2003.ISBN:9788535212068. ↩︎
https://www.oracle.com/news/announcement/oracle-releases-java-20-2023-03-21/ ↩︎
Oracle. Code Conventions for the Java Programming Language: 9. Naming Conventions https://www.oracle.com/java/technologies/javase/codeconventions-namingconventions.html (Acessado em 17/07/2021) ↩︎
Caelum. Java e Orientação a Objetos - Curso fj-11. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
K19-Treinamentos. (2013). Orientação a Objetos em Java, 220. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Bacalá, Sílvio. Página do Professor Sílvio Bacalá Júnior. http://www.facom.ufu.br/~bacala/POO/ ↩︎
Conversões em Java https://www.devmedia.com.br/conversoes-em-java/2695. (Acessado em 05/10/2022) ↩︎
Jakob Jenkov. Tutorials for Software Developers and Technopreneurs! http://tutorials.jenkov.com/. (Acessado em 03/11/2021) ↩︎
Java Collections: Dominando Listas, Sets e Mapas. https://www.alura.com.br/conteudo/java-collections--amp. (Acessado em 21/10/2022) ↩︎
API de data do Java: domine o uso de data no seu código https://www.zup.com.br/blog/api-de-data-do-java ↩︎
Takenami, Igor. Introdução a Programação Orientada a Objetos. Salvador. 2011. (Apostila). ↩︎
https://www.cinqict.nl/blog/sealed-classes-and-functional-programming-in-java ↩︎ ↩︎ ↩︎
https://www.luisllamas.es/en/what-is-a-sealed-class-in-programming/ ↩︎ ↩︎
https://www.javacodegeeks.com/2024/08/a-deep-dive-into-sealed-classes-and-interfaces.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
https://www.linkedin.com/pulse/java-tip-10-sealed-classes-controlled-inheritance-better-orsine-2luof ↩︎ ↩︎ ↩︎ ↩︎
https://ironpdf.com/blog/net-help/csharp-sealed-class-guide/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
https://dev.to/myexamcloud/sealed-class-rules-in-java-225h ↩︎
https://www.c-sharpcorner.com/article/what-is-sealed-class-in-c-sharp/ ↩︎ ↩︎ ↩︎
https://softwareengineering.stackexchange.com/questions/279028/low-coupling-when-using-sealed-classes ↩︎ ↩︎ ↩︎
https://www.linkedin.com/pulse/sealed-enum-class-kotlin-amit-nadiger ↩︎