OOP em Java: Classes, objetos, encapsulamento, herança e abstração
Aprenda programação orientada a objetos em Java com exemplos práticos. Classes mestras, objetos, herança, encapsulamento e classes abstratas usando um sistema de menu de restaurante.
O Java é consistentemente uma das três linguagens mais populares do mundo. Sua adoção em áreas como desenvolvimento de software empresarial, aplicativos móveis Android e aplicativos da Web de grande escala é inigualável. Seu sistema de tipos robusto, o extenso ecossistema e o recurso "escreva uma vez, execute em qualquer lugar" o tornam particularmente atraente para a criação de sistemas robustos e dimensionáveis. Neste artigo, exploraremos como os recursos de programação orientada a objetos do Java permitem que os desenvolvedores aproveitem esses recursos de forma eficaz, possibilitando a criação de aplicativos sustentáveis e dimensionáveis por meio da organização e reutilização adequadas do código.
Uma observação sobre como organizar e executar o código Java
Antes de começarmos a escrever qualquer código, vamos fazer algumas configurações.
Assim como em sua sintaxe, o Java tem regras rígidas sobre a organização do código.
Primeiro, cada classe pública deve estar em seu próprio arquivo, nomeado exatamente como a classe, mas com uma extensão.java. Portanto, se eu quiser escrever uma classe de laptop, o nome do arquivo deverá ser Laptop.java-com distinção entre maiúsculas e minúsculas. Você pode ter classes não públicas no mesmo arquivo, mas é melhor separá-las. Sei que estamos nos adiantando - falando sobre organizar as aulas antes mesmo de escrevê-las -, mas ter uma ideia aproximada de onde colocar as coisas de antemão é uma boa ideia.
Todos os projetos Java devem ter um arquivo Main.java com a classeMain. É aqui que você testa suas classes criando objetos a partir delas.
Para executar o código Java, usaremos oIntelliJ IDEAum IDE Java popular. Depois de instalar o IntelliJ:
Crie um novo projeto Java (Arquivo > Novo > Projeto)
Clique com o botão direito do mouse na pasta src para criar o arquivo Main.java e cole o conteúdo a seguir:
publicclassMain{publicstaticvoidmain(String[] args){// Create and tests objects here}}Tecnologia
Sempre que estamos falando de classes, escrevemos código em outros arquivos além do arquivoMain.java. Mas se estivermos falando sobre a criação e o teste deobjetos, mudaremos para Main.java.
Para executar o programa, você pode clicar no botão verde de reprodução ao lado do método principal:
A saída será mostrada na janela da ferramenta Executar, na parte inferior.
Se você é completamente novo em Java, confira nossoCurso de introdução ao Javaque aborda os fundamentos dos tipos de dados e do fluxo de controle do Java antes de continuar.
Caso contrário, vamos mergulhar de cabeça.
Classes e objetos Java
Então, o que são aulas, exatamente?
As classes são construções de programação em Java para representar conceitos do mundo real. Por exemplo, considere esta classeMenuItem (crie um arquivo para escrever essa classe em seu IDE):
A classe nos dá um plano ou modelo para representar vários itens de menu em um restaurante. Ao alterar os dois atributos da classe, name e price, podemos criar inúmeros objetos de menu , como um hambúrguer ou uma salada.
Portanto, para criar uma classe em Java, você inicia uma linha que descreve o nível de acesso da classe (private,public, ou protected) seguido do nome da classe. Imediatamente após os colchetes, você delineia os atributos da sua classe.
Mas como criamos objetos que pertencem a essa classe? O Java permite isso por meio de métodos de construtor:
Um construtor é um método especial que é chamado quando criamos um novo objeto a partir de uma classe. Ele inicializa os atributos do objeto com os valores que fornecemos. No exemplo acima, o construtor recebe um parâmetro de nome e preço e os atribui aos campos do objeto usando a palavra-chave 'this' para se referir a uma instância futura do objeto.
A sintaxe do construtor é diferente de outros métodos de classe porque não exige que você especifique um tipo de retorno. Além disso, o construtor deve ter o mesmo nome da classe e deve ter o mesmo número de atributos que você declarou após a definição da classe. Acima, o construtor está criando dois atributos porque declaramos dois após a definição da classe: name e price.
Depois de escrever sua classe e seu construtor, você pode criar instâncias (objetos) dela em seu método principal:
Acima, estamos criando dois objetos MenuItem nas variáveis burger e salad. Conforme exigido em Java, o tipo da variável deve ser declarado, que é MenuItem. Em seguida, para criar uma instância da nossa classe, escrevemos a palavra-chavenew seguida da invocação do método construtor.
Além do construtor, você pode criar métodos regulares que dão comportamento à sua classe. Por exemplo, abaixo, adicionamos um método para calcular o preço total após o imposto:
O objetivo das classes é fornecer um modelo para a criação de objetos. Esses objetos serão usados por outros scripts ou programas. Por exemplo, nossos objetosMenuItem podem ser usados por uma interface de usuário que exibe seu nome, preço e imagem em uma tela.
Por esse motivo, devemos projetar nossas classes de modo que suas instâncias só possam ser usadas como pretendemos. No momento, nossa classe MenuItem é muito básica e propensa a erros. Uma pessoa pode criar objetos com atributos ridículos, como uma torta de maçã com preço negativo ou um sanduíche de um milhão de dólares:
Portanto, a primeira coisa a fazer depois de escrever uma classe é proteger seus atributos, limitando a forma como eles são criados e acessados. Para começar, queremos permitir apenas valores positivos para price e definir um valor máximo para evitar a exibição acidental de itens ridiculamente caros.
O Java nos permite fazer isso usando métodos setter:
Vamos examinar o que há de novo no bloco de código acima:
1. Tornamos os atributos privados adicionando a palavra-chave private. Isso significa que eles só podem ser acessados dentro da classe MenuItem. O encapsulamento começa com essa etapa crucial.
2. Adicionamos uma nova constante MAX_PRICE que é:
private (acessível somente dentro da classe)
estático (compartilhado em todas as instâncias)
final (não pode ser alterado após a inicialização)
definido como $100,0 como um preço máximo razoável
3. Adicionamos um métodosetPrice() que:
Recebe um parâmetro de preço
Valida que o preço não é negativo
Valida se o preço não excede MAX_PRICE
Lança IllegalArgumentException com mensagens descritivas se a validação falhar
Somente define o preço se todas as validações forem aprovadas
4. Modificamos o construtor para usar setPrice() em vez de atribuir diretamente o preço. Isso garante que a validação de preço ocorra durante a criação do objeto.
Acabamos de implementar um dos principais pilares de um bom design orientado a objetos:o encapsulamento. Esse paradigma impõe a ocultação de dados e o acesso controlado aos atributos do objeto, garantindo que os detalhes da implementação interna sejam protegidos contra interferência externa e só possam ser modificados por meio de interfaces bem definidas.
Vamos esclarecer o ponto aplicando o encapsulamento ao atributoname. Imagine que temos uma cafeteria que só serve lattes, cappuccinos, expressos, americanos e mochas.
Portanto, nossos nomes de itens de menu só podem ser um dos itens dessa lista. Veja como podemos aplicar isso no código:
// Rest of the class here...privatestaticfinalString[]VALID_NAMES={"latte","cappuccino","espresso","americano","mocha"};privateString name;publicvoidsetName(String name){String lowercaseName = name.toLowerCase();for(String validName :VALID_NAMES){if(validName.equals(lowercaseName)){this.name = name;return;}}thrownewIllegalArgumentException("Invalid drink name. Must be one of: "+String.join(", ",VALID_NAMES));}Tecnologia
O código acima implementa a validação de nome para itens de menu em uma cafeteria. Vamos detalhar isso:
1. Primeiro, ele define um array final estático privado VALID_NAMES que contém os únicos nomes de bebidas permitidos: latte, cappuccino, espresso, americano e mocha. Essa matriz é:
private: acessível somente dentro da classe
estático: compartilhado em todas as instâncias
final: não pode ser modificado após a inicialização
2. Ele declara um campo privado String name para armazenar o nome da bebida
3. O método setName() implementa a lógica de validação:
Recebe um parâmetro de nome String
Converte-o em letras minúsculas para que a comparação não diferencie maiúsculas de minúsculas
Você percorre a matriz VALID_NAMES
Se for encontrada uma correspondência, você definirá o nome e retornará
Se nenhuma correspondência for encontrada, você lançará uma IllegalArgumentException com uma mensagem descritiva listando todas as opções válidas
Aqui está a classe completa até o momento:
publicclassMenuItem{privateString name;privatedouble price;privatestaticfinalString[]VALID_NAMES={"latte","cappuccino","espresso","americano","mocha"};privatestaticfinaldoubleMAX_PRICE=100.0;publicMenuItem(String name,double price){setName(name);setPrice(price);}publicvoidsetName(String name){String lowercaseName = name.toLowerCase();for(String validName :VALID_NAMES){if(validName.equals(lowercaseName)){this.name = name;return;}}thrownewIllegalArgumentException("Invalid drink name. Must be one of: "+String.join(", ",VALID_NAMES));}publicvoidsetPrice(double price){if(price <0){thrownewIllegalArgumentException("Price cannot be negative");}if(price >MAX_PRICE){thrownewIllegalArgumentException("Price cannot exceed $"+MAX_PRICE);}this.price = price;}}Tecnologia
Depois de protegermos a forma como os atributos são criados, também queremos proteger a forma como eles são acessados. Isso é feito por meio de métodos getter:
publicclassMenuItem{// Rest of the code here...publicStringgetName(){return name;}publicdoublegetPrice(){return price;}}Tecnologia
Os métodos getter fornecem acesso controlado a atributos privados de uma classe. Eles resolvem o problema do acesso direto a atributos, que pode levar a modificações indesejadas e quebrar o encapsulamento.
Por exemplo, sem getters, podemos acessar os atributos diretamente:
MenuItem item =newMenuItem("Latte",4.99);String name = item.name;// Direct access to attribute
item.name ="INVALID";// Can modify directly, bypassing validationTecnologia
Com os getters, aplicamos o acesso adequado:
MenuItem item =newMenuItem("Latte",4.99);String name = item.getName();// Controlled access through getter// item.name = "INVALID"; // Not allowed - must use setName() which validatesTecnologia
Esse encapsulamento:
Protege a integridade dos dados, evitando modificações inválidas
Permite que você altere a implementação interna sem afetar o código que usa a classe
Fornece um único ponto de acesso que pode incluir lógica adicional, se necessário
Torna o código mais fácil de manter e menos propenso a bugs
Herança
Nossa classe está começando a parecer boa, mas há muitos problemas com ela. Por exemplo, para um restaurante grande que serve muitos tipos de pratos e bebidas, a classe não é suficientemente flexível.
Se quisermos adicionar diferentes tipos de itens alimentares, enfrentaremos vários desafios. Alguns pratos podem ser preparados para levar, enquanto outros precisam ser consumidos imediatamente. Os itens do cardápio podem ter preços e descontos variados. Os pratos podem precisar de controle de temperatura ou armazenamento especial. As bebidas podem ser quentes ou frias com ingredientes personalizáveis. Os itens podem precisar de informações sobre alergênicos e opções de porções. O sistema atual não lida com esses requisitos variados.
A herança oferece uma solução elegante para todos esses problemas. Ele permite que você crie versões especializadas de itens de menu definindo uma classe MenuItem básica com atributos comuns e, em seguida, criando classes filhas que herdam esses atributos básicos e adicionam recursos exclusivos.
Por exemplo, poderíamos ter uma classe Drink para bebidas com opções de temperatura, uma classe Food para itens que precisam de consumo imediato e uma classe Dessert para itens com necessidades especiais de armazenamento, todas herdando a funcionalidade principal do item de menu.
Extensão de classes
Vamos implementar essas ideias começando com Drink:
Para definir uma classe filha que herda de uma classe mãe, usamos a palavra-chaveextends após o nome da classe filha, seguida da classe mãe. Após a definição da classe, definimos quaisquer novos atributos que esse filho tenha e implementamos seu construtor.
Mas observe como temos que repetir a inicialização de name e price junto com isCold. Isso não é ideal porque a classe principal pode ter centenas de atributos. Além disso, o código acima gerará um erro quando você o compilar, pois essa não é a maneira correta de inicializar os atributos da classe pai. A maneira correta seria usar a palavra-chave super:
A palavra-chavesuper é usada para chamar o construtor da classe principal. Nesse caso, o site super(name, price) chama o construtor deMenuItem para inicializar esses atributos, evitando a duplicação de código. Você só precisa inicializar o novo atributo isCold específico da classeDrink.
A palavra-chave é muito flexível porque você pode usá-la para fazer referência à classe pai em qualquer parte da classe filha. Por exemplo, para chamar um método pai, você usa super.methodName(), enquanto super.attributeName é para atributos.
Sobreposição de métodos
Agora, digamos que você queira adicionar um novo método às nossas classes para calcular o preço total após o imposto. Como diferentes itens do menu podem ter taxas de imposto diferentes (por exemplo, alimentos preparados versus bebidas embaladas), podemos usar a substituição de método para implementar cálculos de imposto específicos em cada classe filha, mantendo um nome de método comum na classe pai.
Aqui está a aparência disso:
publicclassMenuItem{// Rest of the MenuItem classpublicdoublecalculateTotalPrice(){// Default tax rate of 10%return price *1.10;}}publicclassFoodextendsMenuItem{privateboolean isVegetarian;publicFood(String name,double price,boolean isVegetarian){super(name, price);this.isVegetarian = isVegetarian;}@OverridepublicdoublecalculateTotalPrice(){// Food has 15% taxreturnsuper.getPrice()*1.15;}}publicclassDrinkextendsMenuItem{privateboolean isCold;publicDrink(String name,double price,boolean isCold){super(name, price);this.isCold = isCold;}@OverridepublicdoublecalculateTotalPrice(){// Drinks have 8% taxreturnsuper.getPrice()*1.08;}}Tecnologia
Neste exemplo, a substituição de métodos permite que cada subclasse forneça sua própria implementação de calculateTotalPrice():
A classe base MenuItem define um cálculo de imposto padrão de 10%.
Quando Food e Drink estendem MenuItemeles substituem esse método para implementar suas próprias taxas de imposto:
Os itens alimentícios têm uma alíquota de 15% mais alta
As bebidas têm uma taxa de imposto menor, de 8%
A anotação@Override é usada para indicar explicitamente que esses métodos estão substituindo o método da classe principal. Isso ajuda a detectar erros se a assinatura do método não corresponder à classe principal.
Cada subclasse ainda pode acessar o preço da classe principal usando super.getPrice()demonstrando como os métodos substituídos podem utilizar a funcionalidade da classe principal e, ao mesmo tempo, adicionar seu próprio comportamento.
Em resumo, a substituição de métodos é uma parte integrante da herança que permite que as subclasses forneçam sua implementação de métodos definidos na classe principal, possibilitando um comportamento mais específico e mantendo a mesma assinatura de método.
Classes abstratas
Nossa hierarquia de classes MenuItem funciona, mas há um problema: qualquer pessoa deve ser capaz de criar um objetoMenuItem simples ? Afinal, em nosso restaurante, cada item do cardápio é um alimento ou uma bebida - não existe apenas um "item genérico do cardápio".
Você pode evitar isso transformando o siteMenuItem em uma classe abstrata. Uma classe abstrata fornece apenas um modelo de base - ela só pode ser usada como uma classe pai para herança, não pode ser instanciada diretamente.
Para tornar MenuItem abstrato, adicionamos a palavra-chave abstract após seu modificador de acesso:
publicabstractclassMenuItem{privateString name;privatedouble price;publicMenuItem(String name,double price){setName(name);setPrice(price);}// Existing getters/setters remain the same// Make this method abstract - every subclass MUST implement itpublicabstractdoublecalculateTotalPrice();}Tecnologia
As classes abstratas também podem ter métodos abstratos, como calculateTotalPrice() acima. Esses métodos abstratos servem como contratos que forçam as subclasses a fornecer suas implementações. Em outras palavras, qualquer método abstrato em uma classe abstrata deve ser implementado por classes filhas.
Então, vamos reescrever Food e Drink com essas alterações em mente:
Por meio dessa implementação do sistema de menus, vimos como a abstração e a herança trabalham juntas para criar um código flexível e de fácil manutenção que pode se adaptar facilmente a diferentes requisitos comerciais.
Conclusão
Hoje, demos uma olhada no que o Java é capaz de fazer como uma linguagem de programação orientada a objetos. Cobrimos os conceitos básicos de classes, objetos e alguns dos principais pilares da OOP: encapsulamento, herança e abstração por meio de um sistema de menu de restaurante.
Para tornar esse sistema pronto para produção, você ainda tem muitas coisas para aprender, como interfaces (parte da abstração), polimorfismo e padrões de design de OOP. Para saber mais sobre esses conceitos, consulte nossaIntrodução à OOP em Java para você.
Quais são os pré-requisitos para você seguir este tutorial de Java OOP?⌄
Você deve ter conhecimento básico de programação Java, incluindo tipos de dados, variáveis, fluxo de controle (if/else, loops) e sintaxe básica. Nosso curso Introduction to Java aborda esses fundamentos se você precisar de uma atualização.
Por que usar um sistema de menu de restaurante para explicar os conceitos de OOP?⌄
Um sistema de cardápio de restaurante é um exemplo intuitivo que demonstra aplicações reais dos princípios de OOP. Ele mostra naturalmente a herança (diferentes tipos de itens de menu), o encapsulamento (protegendo os valores de preço e nome) e as interfaces (programas de fidelidade, controle de temperatura). A maioria das pessoas entende como os restaurantes funcionam, o que facilita a compreensão dos conceitos.
Qual é a diferença entre classes abstratas e interfaces em Java?⌄
Uma classe pode estender apenas uma classe abstrata, mas implementar várias interfaces.
As classes abstratas podem ter campos e construtores; as interfaces não podem.
As classes abstratas podem ter métodos abstratos e concretos; as interfaces tradicionalmente só têm métodos abstratos (antes do Java 8).
As classes abstratas fornecem uma implementação básica, enquanto as interfaces definem um contrato.
Aprenda sobre classes abstratas em Python, sua finalidade e como usar o módulo `abc` para garantir interfaces consistentes. Inclui exemplos práticos e melhores práticas para uma implementação eficaz.
Em Python, tudo é objeto. Números, cadeias de caracteres (strings), DataFrames, e até mesmo funções são objetos. Especificamente, qualquer coisa que você usa no Python tem uma classe, um modelo associado por trás.
Aprenda os principais conceitos da programação orientada a objetos, desde classes e objetos básicos até tópicos avançados, como herança e polimorfismo.
O Java é consistentemente uma das três linguagens mais populares do mundo. Sua adoção em áreas como desenvolvimento de software empresarial, aplicativos móveis Android e aplicativos da Web de grande escala é inigualável. Seu sistema de tipos robusto, o extenso ecossistema e o recurso "escreva uma vez, execute em qualquer lugar" o tornam particularmente atraente para a criação de sistemas robustos e dimensionáveis. Neste artigo, exploraremos como os recursos de programação orientada a objetos do Java permitem que os desenvolvedores aproveitem esses recursos de forma eficaz, possibilitando a criação de aplicativos sustentáveis e dimensionáveis por meio da organização e reutilização adequadas do código.
Uma observação sobre como organizar e executar o código Java
Antes de começarmos a escrever qualquer código, vamos fazer algumas configurações.
Assim como em sua sintaxe, o Java tem regras rígidas sobre a organização do código.
Primeiro, cada classe pública deve estar em seu próprio arquivo, nomeado exatamente como a classe, mas com uma extensão
.java. Portanto, se eu quiser escrever uma classe de laptop , o nome do arquivo deverá serLaptop.java-com distinção entre maiúsculas e minúsculas. Você pode ter classes não públicas no mesmo arquivo, mas é melhor separá-las. Sei que estamos nos adiantando - falando sobre organizar as aulas antes mesmo de escrevê-las -, mas ter uma ideia aproximada de onde colocar as coisas de antemão é uma boa ideia.Todos os projetos Java devem ter um arquivo
Main.javacom a classeMain. É aqui que você testa suas classes criando objetos a partir delas.Para executar o código Java, usaremos o IntelliJ IDEAum IDE Java popular. Depois de instalar o IntelliJ:
Main.javae cole o conteúdo a seguir:Sempre que estamos falando de classes, escrevemos código em outros arquivos além do arquivo
Main.java. Mas se estivermos falando sobre a criação e o teste deobjetos , mudaremos paraMain.java.Para executar o programa, você pode clicar no botão verde de reprodução ao lado do método principal:
A saída será mostrada na janela da ferramenta Executar, na parte inferior.
Se você é completamente novo em Java, confira nosso Curso de introdução ao Javaque aborda os fundamentos dos tipos de dados e do fluxo de controle do Java antes de continuar.
Caso contrário, vamos mergulhar de cabeça.
Classes e objetos Java
Então, o que são aulas, exatamente?
As classes são construções de programação em Java para representar conceitos do mundo real. Por exemplo, considere esta classe MenuItem (crie um arquivo para escrever essa classe em seu IDE):
A classe nos dá um plano ou modelo para representar vários itens de menu em um restaurante. Ao alterar os dois atributos da classe,
nameeprice, podemos criar inúmeros objetos de menu , como um hambúrguer ou uma salada.Portanto, para criar uma classe em Java, você inicia uma linha que descreve o nível de acesso da classe (
private,public, ouprotected) seguido do nome da classe. Imediatamente após os colchetes, você delineia os atributos da sua classe.Mas como criamos objetos que pertencem a essa classe? O Java permite isso por meio de métodos de construtor:
Um construtor é um método especial que é chamado quando criamos um novo objeto a partir de uma classe. Ele inicializa os atributos do objeto com os valores que fornecemos. No exemplo acima, o construtor recebe um parâmetro de nome e preço e os atribui aos campos do objeto usando a palavra-chave 'this' para se referir a uma instância futura do objeto.
A sintaxe do construtor é diferente de outros métodos de classe porque não exige que você especifique um tipo de retorno. Além disso, o construtor deve ter o mesmo nome da classe e deve ter o mesmo número de atributos que você declarou após a definição da classe. Acima, o construtor está criando dois atributos porque declaramos dois após a definição da classe:
nameeprice.Depois de escrever sua classe e seu construtor, você pode criar instâncias (objetos) dela em seu método principal:
Saída:
Acima, estamos criando dois objetos
MenuItemnas variáveisburgeresalad. Conforme exigido em Java, o tipo da variável deve ser declarado, que éMenuItem. Em seguida, para criar uma instância da nossa classe, escrevemos a palavra-chave new seguida da invocação do método construtor.Além do construtor, você pode criar métodos regulares que dão comportamento à sua classe. Por exemplo, abaixo, adicionamos um método para calcular o preço total após o imposto:
Agora podemos calcular o preço total, incluindo impostos:
Saída:
Encapsulamento
O objetivo das classes é fornecer um modelo para a criação de objetos. Esses objetos serão usados por outros scripts ou programas. Por exemplo, nossos objetos
MenuItempodem ser usados por uma interface de usuário que exibe seu nome, preço e imagem em uma tela.Por esse motivo, devemos projetar nossas classes de modo que suas instâncias só possam ser usadas como pretendemos. No momento, nossa classe
MenuItemé muito básica e propensa a erros. Uma pessoa pode criar objetos com atributos ridículos, como uma torta de maçã com preço negativo ou um sanduíche de um milhão de dólares:Portanto, a primeira coisa a fazer depois de escrever uma classe é proteger seus atributos, limitando a forma como eles são criados e acessados. Para começar, queremos permitir apenas valores positivos para price e definir um valor máximo para evitar a exibição acidental de itens ridiculamente caros.
O Java nos permite fazer isso usando métodos setter:
Vamos examinar o que há de novo no bloco de código acima:
1. Tornamos os atributos privados adicionando a palavra-chave
private. Isso significa que eles só podem ser acessados dentro da classe MenuItem. O encapsulamento começa com essa etapa crucial.2. Adicionamos uma nova constante
MAX_PRICEque é:3. Adicionamos um método
setPrice()que:4. Modificamos o construtor para usar
setPrice()em vez de atribuir diretamente o preço. Isso garante que a validação de preço ocorra durante a criação do objeto.Acabamos de implementar um dos principais pilares de um bom design orientado a objetos:o encapsulamento. Esse paradigma impõe a ocultação de dados e o acesso controlado aos atributos do objeto, garantindo que os detalhes da implementação interna sejam protegidos contra interferência externa e só possam ser modificados por meio de interfaces bem definidas.
Vamos esclarecer o ponto aplicando o encapsulamento ao atributoname. Imagine que temos uma cafeteria que só serve lattes, cappuccinos, expressos, americanos e mochas.
Portanto, nossos nomes de itens de menu só podem ser um dos itens dessa lista. Veja como podemos aplicar isso no código:
O código acima implementa a validação de nome para itens de menu em uma cafeteria. Vamos detalhar isso:
1. Primeiro, ele define um array final estático privado
VALID_NAMESque contém os únicos nomes de bebidas permitidos: latte, cappuccino, espresso, americano e mocha. Essa matriz é:2. Ele declara um campo privado String name para armazenar o nome da bebida
3. O método
setName()implementa a lógica de validação:VALID_NAMESAqui está a classe completa até o momento:
Depois de protegermos a forma como os atributos são criados, também queremos proteger a forma como eles são acessados. Isso é feito por meio de métodos getter:
Os métodos getter fornecem acesso controlado a atributos privados de uma classe. Eles resolvem o problema do acesso direto a atributos, que pode levar a modificações indesejadas e quebrar o encapsulamento.
Por exemplo, sem getters, podemos acessar os atributos diretamente:
Com os getters, aplicamos o acesso adequado:
Esse encapsulamento:
Herança
Nossa classe está começando a parecer boa, mas há muitos problemas com ela. Por exemplo, para um restaurante grande que serve muitos tipos de pratos e bebidas, a classe não é suficientemente flexível.
Se quisermos adicionar diferentes tipos de itens alimentares, enfrentaremos vários desafios. Alguns pratos podem ser preparados para levar, enquanto outros precisam ser consumidos imediatamente. Os itens do cardápio podem ter preços e descontos variados. Os pratos podem precisar de controle de temperatura ou armazenamento especial. As bebidas podem ser quentes ou frias com ingredientes personalizáveis. Os itens podem precisar de informações sobre alergênicos e opções de porções. O sistema atual não lida com esses requisitos variados.
A herança oferece uma solução elegante para todos esses problemas. Ele permite que você crie versões especializadas de itens de menu definindo uma classe
MenuItembásica com atributos comuns e, em seguida, criando classes filhas que herdam esses atributos básicos e adicionam recursos exclusivos.Por exemplo, poderíamos ter uma classe
Drinkpara bebidas com opções de temperatura, uma classeFoodpara itens que precisam de consumo imediato e uma classeDessertpara itens com necessidades especiais de armazenamento, todas herdando a funcionalidade principal do item de menu.Extensão de classes
Vamos implementar essas ideias começando com
Drink:Para definir uma classe filha que herda de uma classe mãe, usamos a palavra-chave
extendsapós o nome da classe filha, seguida da classe mãe. Após a definição da classe, definimos quaisquer novos atributos que esse filho tenha e implementamos seu construtor.Mas observe como temos que repetir a inicialização de
nameepricejunto comisCold. Isso não é ideal porque a classe principal pode ter centenas de atributos. Além disso, o código acima gerará um erro quando você o compilar, pois essa não é a maneira correta de inicializar os atributos da classe pai. A maneira correta seria usar a palavra-chavesuper:A palavra-chave
superé usada para chamar o construtor da classe principal. Nesse caso, o sitesuper(name, price)chama o construtor deMenuItempara inicializar esses atributos, evitando a duplicação de código. Você só precisa inicializar o novo atributoisColdespecífico da classeDrink.A palavra-chave é muito flexível porque você pode usá-la para fazer referência à classe pai em qualquer parte da classe filha. Por exemplo, para chamar um método pai, você usa
super.methodName(), enquantosuper.attributeNameé para atributos.Sobreposição de métodos
Agora, digamos que você queira adicionar um novo método às nossas classes para calcular o preço total após o imposto. Como diferentes itens do menu podem ter taxas de imposto diferentes (por exemplo, alimentos preparados versus bebidas embaladas), podemos usar a substituição de método para implementar cálculos de imposto específicos em cada classe filha, mantendo um nome de método comum na classe pai.
Aqui está a aparência disso:
Neste exemplo, a substituição de métodos permite que cada subclasse forneça sua própria implementação de
calculateTotalPrice():A classe base
MenuItemdefine um cálculo de imposto padrão de 10%.Quando
FoodeDrinkestendemMenuItemeles substituem esse método para implementar suas próprias taxas de imposto:A anotação
@Overrideé usada para indicar explicitamente que esses métodos estão substituindo o método da classe principal. Isso ajuda a detectar erros se a assinatura do método não corresponder à classe principal.Cada subclasse ainda pode acessar o preço da classe principal usando
super.getPrice()demonstrando como os métodos substituídos podem utilizar a funcionalidade da classe principal e, ao mesmo tempo, adicionar seu próprio comportamento.Em resumo, a substituição de métodos é uma parte integrante da herança que permite que as subclasses forneçam sua implementação de métodos definidos na classe principal, possibilitando um comportamento mais específico e mantendo a mesma assinatura de método.
Classes abstratas
Nossa hierarquia de classes
MenuItemfunciona, mas há um problema: qualquer pessoa deve ser capaz de criar um objetoMenuItemsimples ? Afinal, em nosso restaurante, cada item do cardápio é um alimento ou uma bebida - não existe apenas um "item genérico do cardápio".Você pode evitar isso transformando o site
MenuItemem uma classe abstrata. Uma classe abstrata fornece apenas um modelo de base - ela só pode ser usada como uma classe pai para herança, não pode ser instanciada diretamente.Para tornar
MenuItemabstrato, adicionamos a palavra-chaveabstractapós seu modificador de acesso:As classes abstratas também podem ter métodos abstratos, como
calculateTotalPrice()acima. Esses métodos abstratos servem como contratos que forçam as subclasses a fornecer suas implementações. Em outras palavras, qualquer método abstrato em uma classe abstrata deve ser implementado por classes filhas.Então, vamos reescrever
FoodeDrinkcom essas alterações em mente:Por meio dessa implementação do sistema de menus, vimos como a abstração e a herança trabalham juntas para criar um código flexível e de fácil manutenção que pode se adaptar facilmente a diferentes requisitos comerciais.
Conclusão
Hoje, demos uma olhada no que o Java é capaz de fazer como uma linguagem de programação orientada a objetos. Cobrimos os conceitos básicos de classes, objetos e alguns dos principais pilares da OOP: encapsulamento, herança e abstração por meio de um sistema de menu de restaurante.
Para tornar esse sistema pronto para produção, você ainda tem muitas coisas para aprender, como interfaces (parte da abstração), polimorfismo e padrões de design de OOP. Para saber mais sobre esses conceitos, consulte nossa Introdução à OOP em Java para você.
Se você quiser testar seus conhecimentos de Java, tente responder a algumas das perguntas do em nosso artigo Perguntas de entrevista sobre Java.