Pular para o conteúdo principal

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.
Actualizado 14 de fev. de 2025  · 15 min de leitura

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 o IntelliJ IDEAum IDE Java popular. Depois de instalar o IntelliJ:

  1. Crie um novo projeto Java (Arquivo > Novo > Projeto)
  2. Clique com o botão direito do mouse na pasta src para criar o arquivo Main.java e cole o conteúdo a seguir:
public class Main {
   public static void main(String[] args) {
       // Create and tests objects here
      
   }
}

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:

Uma captura de tela de uma janela do editor no IntelliJ IDEA para Java

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):

public class MenuItem {
   public String name;
   public double price;
}

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:

public class MenuItem {
   public String name;
   public double price;
  
   // Constructor
   public MenuItem(String name, double price) {
       this.name = name;
       this.price = price;
   }
}

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:

public class Main {
   public static void main(String[] args) {
       // Create objects here
       MenuItem burger = new MenuItem("Burger", 3.5);
       MenuItem salad = new MenuItem("Salad", 2.5);
       System.out.println(burger.name + ", " + burger.price);
   }
}

Saída:

Burger, 3.5

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-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:

public class MenuItem {
   public String name;
   public double price;
  
   // Constructor
   public MenuItem(String name, double price) {
       this.name = name;
       this.price = price;
   }
  
   // Method to calculate price after tax
   public double getPriceAfterTax() {
       double taxRate = 0.08; // 8% tax rate
       return price + (price * taxRate);
   }
}

Agora podemos calcular o preço total, incluindo impostos:

public class Main {
   public static void main(String[] args) {
       MenuItem burger = new MenuItem("Burger", 3.5);
       System.out.println("Price after tax: $" + burger.getPriceAfterTax());
   }
}

Saída:

Price after tax: $3.78

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 MenuItem 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:

// Inside Main.java
MenuItem applePie = new MenuItem("Apple Pie", -5.99);  // Negative price!
MenuItem sandwich = new MenuItem("Sandwich", 1000000); // Unreasonably expensive

System.out.println("Apple pie price: $" + applePie.price);
System.out.println("Sandwich price: $" + sandwich.price);

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:

public class MenuItem {
   private String name;
   private double price;
   private static final double MAX_PRICE = 100.0;
  
   public MenuItem(String name, double price) {
       this.name = name;
       setPrice(price);
   }
  
   public void setPrice(double price) {
       if (price < 0) {
           throw new IllegalArgumentException("Price cannot be negative");
       }
       if (price > MAX_PRICE) {
           throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE);
       }
       this.price = price;
   }
  
}

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étodo setPrice() 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
...
private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"};
private String name;
public void setName(String name) {
   String lowercaseName = name.toLowerCase();
   for (String validName : VALID_NAMES) {
       if (validName.equals(lowercaseName)) {
           this.name = name;
           return;
       }
   }
   throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES));
}

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:

public class MenuItem {
   private String name;
   private double price;

   private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"};

   private static final double MAX_PRICE = 100.0;

   public MenuItem(String name, double price) {
       setName(name);
       setPrice(price);
   }

   public void setName(String name) {
       String lowercaseName = name.toLowerCase();
       for (String validName : VALID_NAMES) {
           if (validName.equals(lowercaseName)) {
               this.name = name;
               return;
           }
       }
       throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES));
   }

   public void setPrice(double price) {
       if (price < 0) {
           throw new IllegalArgumentException("Price cannot be negative");
       }
       if (price > MAX_PRICE) {
           throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE);
       }
       this.price = price;
   }
}

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:

public class MenuItem {
   // Rest of the code here
   ...
   public String getName() {
       return name;
   }
   public double getPrice() {
       return price;
   }
}

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 = new MenuItem("Latte", 4.99);
String name = item.name; // Direct access to attribute
item.name = "INVALID"; // Can modify directly, bypassing validation

Com os getters, aplicamos o acesso adequado:

MenuItem item = new MenuItem("Latte", 4.99);
String name = item.getName(); // Controlled access through getter
// item.name = "INVALID"; // Not allowed - must use setName() which validates

Esse encapsulamento:

  1. Protege a integridade dos dados, evitando modificações inválidas
  2. Permite que você altere a implementação interna sem afetar o código que usa a classe
  3. Fornece um único ponto de acesso que pode incluir lógica adicional, se necessário
  4. 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:

public class Drink extends MenuItem {
   private boolean isCold;
  
   public Drink(String name, double price, boolean isCold) {
       this.name = name;
       this.price = price;
       this.isCold = isCold;
   }
  
   public boolean getIsCold() {
       return isCold;
   }
  
   public void setIsCold(boolean isCold) {
       this.isCold = isCold;
   }
}

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:

public class Drink extends MenuItem {
   private boolean isCold;
  
   public Drink(String name, double price, boolean isCold) {
       super(name, price);
       this.isCold = isCold;
   }
  
   public boolean getIsCold() {
       return isCold;
   }
  
   public void setIsCold(boolean isCold) {
       this.isCold = isCold;
   }
}

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:

public class MenuItem {
   // Rest of the MenuItem class
  
   public double calculateTotalPrice() {
       // Default tax rate of 10%
       return price * 1.10;
   }
}
public class Food extends MenuItem {
   private boolean isVegetarian;
  
   public Food(String name, double price, boolean isVegetarian) {
       super(name, price);
       this.isVegetarian = isVegetarian;
   }
  
   @Override
   public double calculateTotalPrice() {
       // Food has 15% tax
       return super.getPrice() * 1.15;
   }
}
public class Drink extends MenuItem {
   private boolean isCold;
  
   public Drink(String name, double price, boolean isCold) {
       super(name, price);
       this.isCold = isCold;
   }
  
   @Override
   public double calculateTotalPrice() {
       // Drinks have 8% tax
       return super.getPrice() * 1.08;
   }
}

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:

public abstract class MenuItem {
   private String name;
   private double price;
  
   public MenuItem(String name, double price) {
       setName(name);
       setPrice(price);
   }
  
   // Existing getters/setters remain the same
  
   // Make this method abstract - every subclass MUST implement it
   public abstract double calculateTotalPrice();
}

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:

public class Food extends MenuItem {
   private boolean isVegetarian;
  
   public Food(String name, double price, boolean isVegetarian) {
       super(name, price);
       this.isVegetarian = isVegetarian;
   }
  
   @Override
   public double calculateTotalPrice() {
       return getPrice() * 1.15;  // 15% tax
   }
}
public class Drink extends MenuItem {
   private boolean hasCaffeine;
  
   public Drink(String name, double price, boolean hasCaffeine) {
       super(name, price);
       this.hasCaffeine = hasCaffeine;
   }
  
   @Override
   public double calculateTotalPrice() {
       return getPrice() * 1.10;  // 10% tax
   }
}

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.

Perguntas frequentes sobre OOP em Java

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.

Bex Tuychiev's photo
Author
Bex Tuychiev
LinkedIn

Sou um criador de conteúdo de ciência de dados com mais de 2 anos de experiência e um dos maiores seguidores no Medium. Gosto de escrever artigos detalhados sobre IA e ML com um estilo um pouco sarcástico, porque você precisa fazer algo para torná-los um pouco menos monótonos. Produzi mais de 130 artigos e um curso DataCamp, e estou preparando outro. Meu conteúdo foi visto por mais de 5 milhões de pessoas, das quais 20 mil se tornaram seguidores no Medium e no LinkedIn. 

Temas

Principais cursos de Java

Certificação disponível

curso

Introdução ao Java

4 hr
6.8K
Aprenda Java do zero com este curso para iniciantes, dominando conceitos e habilidades essenciais de programação.
Ver DetalhesRight Arrow
Iniciar curso
Ver maisRight Arrow
Relacionado

tutorial

Programação orientada a objetos em Python (OOP): Tutorial

Aborde os fundamentos da programação orientada a objetos (OOP) em Python: explore classes, objetos, métodos de instância, atributos e muito mais!
Théo Vanderheyden's photo

Théo Vanderheyden

12 min

tutorial

Tutorial de Python

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.
DataCamp Team's photo

DataCamp Team

3 min

tutorial

Tutorial de SQLAlchemy com exemplos

Aprenda a acessar e executar consultas SQL em todos os tipos de bancos de dados relacionais usando objetos Python.
Abid Ali Awan's photo

Abid Ali Awan

13 min

tutorial

Operadores em Python

Este tutorial aborda os diferentes tipos de operadores em Python, sobrecarga de operadores, precedência e associatividade.
Théo Vanderheyden's photo

Théo Vanderheyden

9 min

tutorial

Exemplos e tutoriais de consultas SQL

Se você deseja começar a usar o SQL, nós o ajudamos. Neste tutorial de SQL, apresentaremos as consultas SQL, uma ferramenta poderosa que nos permite trabalhar com os dados armazenados em um banco de dados. Você verá como escrever consultas SQL, aprenderá sobre
Sejal Jaiswal's photo

Sejal Jaiswal

21 min

tutorial

Desenvolvimento de back-end em Python: Um guia completo para iniciantes

Este guia completo ensina a você os fundamentos do desenvolvimento de back-end em Python. Aprenda conceitos básicos, estruturas e práticas recomendadas para você começar a criar aplicativos da Web.
Oluseye Jeremiah's photo

Oluseye Jeremiah

26 min

Ver maisVer mais