Estrutura de Dados: Filas (FIFO)
Análise e Implementação de Filas Lineares, Aritmética Modular para Fila Circular e Gestão de Memória em Java
O Conceito FIFO e a Estrutura de Fila
1.1 O que é uma Fila?
A Fila (ou Queue) é uma estrutura de dados linear e restrita que opera sob a disciplina de acesso FIFO (First-In, First-Out) — o primeiro elemento a entrar na estrutura deve ser obrigatoriamente o primeiro a sair.
Diferente das pilhas (onde tudo ocorre no topo) ou de arrays comuns (onde qualquer índice pode ser acessado livremente), em uma fila as operações ocorrem em duas extremidades distintas: a inserção de novos dados é feita sempre no fim (ou traseira/rear) e a remoção ou consulta ocorre sempre no início (ou frente/front).
1.2 Analogias e Funcionamento
A analogia clássica mais intuitiva é a fila de atendimento de um banco: a primeira pessoa que chega é a primeira a ser atendida pelo caixa, enquanto as próximas pessoas aguardam ordenadamente no final da fila.
No ecossistema de software e na arquitetura de sistemas operacionais, esse padrão é essencial em:
- Fila de Impressão (Print Spooler): Garante que os documentos impressos por múltiplos usuários sejam processados estritamente na ordem em que foram enviados.
- Escalonamento de Processos no S.O. (Round-Robin / Ready Queue): O sistema operacional gerencia threads e processos prontos para execução em uma fila de espera da CPU.
- Mensageria e Buffers Assíncronos (RabbitMQ, Apache Kafka, Redis Queue): Permite a comunicação desacoplada entre microsserviços com garantia de processamento ordenado de mensagens.
Vetores Estáticos vs. Fila Circular
2.1 O Modelo de Armazenamento Estático para Filas
Implementar uma fila utilizando vetores estáticos em Java oferece alto desempenho e controle de baixo nível sobre a memória, mas impõe um desafio geométrico peculiar:
Vantagens Técnicas
- Previsibilidade Temporal: Inserções e remoções operam em complexidade de tempo constante O(1), pois os ponteiros acessam posições indexadas diretamente.
- Localidade Espacial: Os elementos residem contiguamente na memória física, garantindo máxima taxa de acerto no Cache L1/L2 do processador.
- Baixo Impacto no Garbage Collector: Não gera criação ou destruição contínua de objetos nó em tempo de execução.
O Problema do Desperdício Linear
Em um vetor linear simples, ao removermos elementos do inicio, o índice da frente avança. As primeiras posições tornam-se espaços mortos inacessíveis. Quando o índice fim chega ao final físico do array, o sistema acusa que a fila está cheia (falso Overflow), mesmo que haja posições livres no início!
2.2 Soluções Arquiteturais
Abordagem 1 Deslocamento de Itens (Shift Array)
Ao remover do início, movemos todos os itens restantes uma posição para a esquerda. Desvantagem: Degrada o desempenho de remoção de O(1) para O(N), tornando a operação inviável para grandes volumes.
Abordagem 2 Fila Circular com Aritmética Modular (Solução Ótima)
Fazemos com que os ponteiros inicio e fim "deem a volta" para o índice 0 ao atingirem a borda do array usando a operação de resto da divisão: (indice + 1) % tamanho. Mantém todas as operações em O(1) sem desperdiçar um único byte!
Abordagem 3 Fila Dinâmica Encadeada (Linked Queue)
Utiliza nós dinâmicos apontados por referências. Excelente para quando a capacidade máxima da fila é imprevisível em tempo de compilação.
Gestão de Erros de Fila: Overflow e Underflow
3.1 O que é Overflow (Estouro)?
Em uma fila de capacidade fixa, o erro de Overflow ocorre quando solicitamos a inserção de um elemento (enfileirar) e a quantidade de dados armazenados já atingiu a capacidade limite da estrutura. Tentar escrever sem checagem causaria sobrescrita indevida ou exceção de índice fora dos limites (ArrayIndexOutOfBoundsException).
3.2 O que é Underflow (Esvaziamento)?
O erro de Underflow ocorre no sentido inverso: quando uma rotina solicita a remoção (desenfileirar) ou a consulta (espiarInicio) de um elemento em uma fila que se encontra completamente vazia (total == 0).
Dica de Projeto Defensivo
Em filas circulares, manter uma variável de controle explícita (ex:totalde elementos) simplifica drasticamente as verificações de estado (vaziaecheia), eliminando a ambiguidade clássica ondeinicio == fimpode significar tanto fila cheia quanto fila vazia.
Os Métodos Fundamentais de Controle
Abaixo está a interface pública padronizada que garante o encapsulamento, robustez e alta coesão na manipulação da Fila Circular:
| Método | Descrição Técnica | Comportamento dos Índices |
|---|---|---|
| enfileirar(valor) / enqueue | Valida se a fila está cheia. Se houver espaço, grava no fim e incrementa total. | fim = (fim + 1) % tamanho |
| desenfileirar() / dequeue | Valida se a fila está vazia. Se contiver itens, recupera o primeiro e decrementa total. | inicio = (inicio + 1) % tamanho |
| espiarInicio() / peek | Consulta e retorna o elemento atual da frente sem removê-lo. | Inalterado |
| verificarFilaVazia() | Verifica se a fila não possui elementos ativos (total == 0). | Inalterado |
| verificarFilaCheia() | Verifica se a fila atingiu a capacidade máxima (total == tamanho). | Inalterado |
| obterTotal() / size | Retorna a contagem atual de elementos armazenados. | Inalterado |
Implementação Completa em Java
Abaixo apresentamos a classe FilaCircular.java com tratamento rigoroso contra Overflow e Underflow utilizando a aritmética modular para reaproveitamento total da memória alocada:
public class FilaCircular {
private int inicio; // Aponta para o elemento que está na frente da fila
private int fim; // Aponta para a próxima posição livre no fim da fila
private int total; // Controla a quantidade real de elementos presentes
private int[] itens; // Vetor que armazena os dados fisicamente
// Construtor que inicializa a fila com capacidade estática
public FilaCircular(int tamanho) {
this.inicio = 0;
this.fim = 0;
this.total = 0;
this.itens = new int[tamanho];
}
// Verifica se a fila está vazia
public boolean verificarFilaVazia() {
return this.total == 0;
}
// Verifica se a fila atingiu sua capacidade física limite
public boolean verificarFilaCheia() {
return this.total == this.itens.length;
}
// Adiciona um valor no fim da fila (ENQUEUE) com proteção contra Overflow
public void enfileirar(int valor) {
if (verificarFilaCheia()) {
System.out.println("[Erro] Overflow! A fila está cheia. Não foi possível inserir: " + valor);
return;
}
this.itens[this.fim] = valor;
// Aritmética Modular: reinicia o índice para 0 ao ultrapassar o fim físico do array
this.fim = (this.fim + 1) % this.itens.length;
this.total++;
System.out.println("Inserido: " + valor + " | Total de elementos na Fila: " + this.total);
}
// Remove o elemento da frente da fila (DEQUEUE) com proteção contra Underflow
public int desenfileirar() {
if (verificarFilaVazia()) {
throw new RuntimeException("[Erro] Underflow! A fila está vazia. Impossível remover.");
}
int valorRemovido = this.itens[this.inicio];
// Avanço circular do ponteiro de início
this.inicio = (this.inicio + 1) % this.itens.length;
this.total--;
System.out.println("Removido: " + valorRemovido + " | Total de elementos restantes: " + this.total);
return valorRemovido;
}
// Consulta o elemento que está na frente (PEEK) sem alterá-lo
public int espiarInicio() {
if (verificarFilaVazia()) {
throw new RuntimeException("A fila está vazia. Não há elementos para consultar.");
}
return this.itens[this.inicio];
}
// Retorna a quantidade atual de itens
public int obterTotal() {
return this.total;
}
}
Estudo de Caso Prático: Testando o Reaproveitamento Circular
No código de teste a seguir, demonstramos na prática como a fila circular aceita novas inserções após remoções parciais, reaproveitando os índices iniciais liberados sem erro de Overflow.
public class AppFila {
public static void main(String[] args) {
System.out.println("--- Execução da Fila Circular ---\n");
// Instancia uma Fila Circular com capacidade para 3 elementos
FilaCircular filaAtendimento = new FilaCircular(3);
// 1. Enfileirando elementos até preencher
filaAtendimento.enfileirar(101); // Senha 101
filaAtendimento.enfileirar(102); // Senha 102
filaAtendimento.enfileirar(103); // Senha 103
// 2. Testando tentativa de estouro (Overflow)
filaAtendimento.enfileirar(104); // Deve acusar fila cheia
System.out.println("\nPróximo a ser atendido: " + filaAtendimento.espiarInicio());
// 3. Atendendo clientes (Desenfileirando 2 posições)
System.out.println("\n--- Atendimento no Caixa ---");
filaAtendimento.desenfileirar(); // Remove 101
filaAtendimento.desenfileirar(); // Remove 102
// 4. Reaproveitamento Circular de Memória
System.out.println("\n--- Testando Inserção Circular nos Espaços Liberados ---");
filaAtendimento.enfileirar(104); // Inserido circularmente!
filaAtendimento.enfileirar(105); // Inserido normalmente!
// 5. Esvaziando totalmente a estrutura
System.out.println("\n--- Descarregando o Restante da Fila ---");
while (!filaAtendimento.verificarFilaVazia()) {
filaAtendimento.desenfileirar();
}
}
}
Prática de Laboratório (Mãos à Obra!)
Desafio Prático: Simulador de Atendimento com Prioridade 2:1
Utilizando a estrutura de FilaCircular, implemente um sistema de controle de atendimento bancário com duas filas distintas: Preferencial (idosos, gestantes) e Comum.
Especificações do Desafio:
- Regra 1: Mantenha duas instâncias de fila:
filaPreferencialefilaComum. - Regra 2 (Proporção 2:1): Para cada 2 clientes atendidos da fila preferencial, atenda obrigatoriamente 1 cliente da fila comum.
- Regra 3 (Fluxo Suave): Se a fila preferencial estiver vazia, atenda os clientes da fila comum sequencialmente sem interrupção. Se a fila comum estiver vazia, continue atendendo a preferencial.
- Regra 4: Crie um método
atenderProximo()na classe controladora que decida a fila correta de acordo com as regras acima e exiba a senha no console.
public class SimuladorAtendimento {
private FilaCircular filaPreferencial;
private FilaCircular filaComum;
private int contadorPreferencial;
public SimuladorAtendimento(int capacidade) {
this.filaPreferencial = new FilaCircular(capacidade);
this.filaComum = new FilaCircular(capacidade);
this.contadorPreferencial = 0;
}
public void adicionarPreferencial(int senha) {
filaPreferencial.enfileirar(senha);
}
public void adicionarComum(int senha) {
filaComum.enfileirar(senha);
}
public void atenderProximo() {
if (filaPreferencial.verificarFilaVazia() && filaComum.verificarFilaVazia()) {
System.out.println("[Atendimento] Ambas as filas estão vazias.");
return;
}
// Atende Preferencial se houver pessoas E (não atingiu a cota de 2 OU a Comum está vazia)
if (!filaPreferencial.verificarFilaVazia() && (contadorPreferencial < 2 || filaComum.verificarFilaVazia())) {
int senha = filaPreferencial.desenfileirar();
contadorPreferencial++;
System.out.println(">> Chamando PREFERENCIAL: Senha " + senha + " (Atendimentos seguidos: " + contadorPreferencial + "/2)");
}
// Caso contrário, atende a Comum se houver pessoas
else if (!filaComum.verificarFilaVazia()) {
int senha = filaComum.desenfileirar();
contadorPreferencial = 0; // Reinicia a contagem de preferenciais
System.out.println(">> Chamando COMUM: Senha " + senha);
}
}
public static void main(String[] args) {
SimuladorAtendimento banco = new SimuladorAtendimento(10);
// Simulando chegada de clientes
banco.adicionarPreferencial(201);
banco.adicionarPreferencial(202);
banco.adicionarPreferencial(203);
banco.adicionarComum(101);
banco.adicionarComum(102);
System.out.println("--- Início do Atendimento Bancário (Regra 2:1) ---");
banco.atenderProximo(); // Preferencial 201 (1/2)
banco.atenderProximo(); // Preferencial 202 (2/2)
banco.atenderProximo(); // Comum 101 (reseta contador)
banco.atenderProximo(); // Preferencial 203 (1/2)
banco.atenderProximo(); // Comum 102 (reseta contador)
}
}
Comparativo Definitivo: FILA vs. PILHA
Sintetize e consolide as distinções estruturais entre as duas principais estruturas de dados sequenciais lineares:
| Característica | FILA (Queue) | PILHA (Stack) |
|---|---|---|
| Protocolo de Acesso | FIFO (First-In, First-Out) | LIFO (Last-In, First-Out) |
| Pontos de Operação | Duas extremidades: insere no fim, remove no início | Uma única extremidade: todas as operações ocorrem no topo |
| Ponteiros de Controle | inicio e fim |
topo |
| Terminologia em Métodos | enfileirar() / desenfileirar() |
empilhar() / desempilhar() |
| Principal Desafio (Vetor) | Desperdício de memória no início (resolvido por Fila Circular) | Estouro de capacidade física (Overflow ao passar do limite) |
| Casos de Uso Típicos | Spooler de impressão, filas de mensagens, agendamento de SO | Desfazer/Refazer (Ctrl+Z), histórico do navegador, Call Stack |
Fim deste Conteúdo
Aqui encerra-se a nossa análise sobre estruturas lineares sequenciais baseadas em vetores estáticos (Pilhas e Filas). Na próxima aula, avançaremos para o estudo de Listas Encadeadas (Alocação Dinâmica e Referências de Memória) em Java.
Materiais Complementares de Apoio
Slides da Aula: Filas (FIFO) & Fila Circular
Acesse o slide de apoio didático estruturado em PDF/PowerPoint, contendo toda a parte visual, esquemas de fila circular e diagramas de classe UML abordados.
Videoaula Relacionada: FILAS - Java
Assista à explicação detalhada com resoluções de problemas e exemplos práticos de implementação de filas circulares.
Repositório de Código Fonte
Explore, acesse e faça o download de todos os códigos-fonte de exemplo e exercícios estruturados nesta trilha.