Formatação de texto

Em Java, é possível formatar textos e números de diversas maneiras. Isso pode ser útil em diversas situações, como ao exibir valores para o usuário de uma maneira mais legível.

Uma das maneiras mais comuns de se formatar textos em Java é utilizando o método format(), da classe String. Esse método permite formatar um texto utilizando diversos placeholders, que são representados pelo caractere % seguido de uma letra que indica o tipo de dado que será inserido no placeholder. Por exemplo, %s indica que uma String será inserida no placeholder, %d indica um valor inteiro e %f indica um valor de ponto flutuante. Vamos ver um exemplo:


                String nome = "Maria";
                int idade = 30;
                double valor = 55.9999;
                System.out.println(String.format("Meu nome é %s, eu tenho %d anos e hoje gastei %.2f reais", nome, idade, valor));

            

Nesse exemplo, os valores das variáveis nome, idade e valor são passados como parâmetros para o método String.format, substituindo os placeholders %s, %d e %.2f, respectivamente. O resultado impresso será "Meu nome é Maria, eu tenho 30 anos e hoje gastei 55,99 reais". Perceba também que o placeholder %.2f indica que o valor deve ser formatado com duas casas decimais.

Esse exemplo do que foi feito para o String.format também pode ser usado com Text Block, onde usa-se o método que citei em aula, o formatted, para informar as variáveis que deverão ser utilizadas no lugar dos placeholders. Veja esse exemplo:


                String nome = "João";
                int aulas = 4;
                
                String mensagem = """
                Olá, %s!
                Boas vindas ao curso de Java.
                Teremos %d aulas para te mostrar o que é preciso para você dar o seu primeiro mergulho na linguagem!
                """.formatted(nome, aulas);
                
                System.out.println(mensagem);
                Copiar código
                O resultado impresso será:
                
                Olá, João!
                
                Boas vindas ao curso de Java.
            

Tipos Primitivos em Java

boolean

O tipo boolean é utilizado para representar valores lógicos, podendo assumir apenas dois valores: true ou false. É utilizado em expressões condicionais, loops e outros casos onde se deseja avaliar se uma determinada condição é verdadeira ou falsa.

byte

O tipo byte é utilizado para representar valores numéricos inteiros de 8 bits. Ele possui uma faixa de valores de -128 a 127.

char

O tipo char é utilizado para representar caracteres individuais. Ele pode armazenar qualquer caractere Unicode e é representado por aspas simples ('').

short

O tipo short é utilizado para representar valores numéricos inteiros de 16 bits. Ele possui uma faixa de valores de -32.768 a 32.767.

int

O tipo int é utilizado para representar valores numéricos inteiros de 32 bits. É um dos tipos de dados mais utilizados para representar números inteiros em Java e possui uma faixa de valores de -2.147.483.648 a 2.147.483.647.

long

O tipo long é utilizado para representar valores numéricos inteiros de 64 bits. Ele é utilizado para representar valores inteiros muito grandes e possui uma faixa de valores de -9.223.372.036.854.775.808 a 9.223.372.036.854.775.807.

float

O tipo float é utilizado para representar valores numéricos de ponto flutuante, ou seja, valores com casas decimais, sendo que ocupa 32 bits de memória. Ele pode representar números decimais com até sete dígitos e tem uma precisão limitada, o que significa que ele pode arredondar os números se eles forem muito grandes ou muito pequenos.

double

O tipo double é similar o float, entretanto ele ocupa 64 bits de memória e pode representar números decimais com até 15 dígitos.

Utilizando Scanner


                public class ExemploScanner {
                    public static void main(String[] args) {
                        Scanner scanner = new Scanner(System.in);
                        
                        System.out.print("Digite seu nome: ");
                        String nome = scanner.nextLine();
                        System.out.print("Digite sua idade: ");
                        int idade = scanner.nextInt();
                        System.out.print("Digite o valor que pretende investir esse mês: ");
                        double valor = scanner.nextDouble();
                        
                        System.out.println(nome + " que tem " + idade + " anos, irá investir R$ " + valor + " esse mês.");
                        
                        scanner.close();
                    }
                }
            

Erro fantasma no scanner

Quando você usa nextDouble() (ou nextInt()), o Scanner lê apenas o número. O caractere de "quebra de linha" (\n), que é gerado quando você aperta Enter, continua parado no "buffer" (a memória temporária do teclado).

Na próxima vez que o código executa o nextLine(), ele encontra esse \n sobrando, acha que você enviou uma linha vazia e "pula" a leitura da descrição, indo direto para o próximo nextDouble().

Muitos programadores preferem ler tudo com nextLine() e converter para o número depois. Isso evita qualquer confusão com o buffer.

double valor = Double.parseDouble(scanner.nextLine());

Utilizando for


                import java.util.Scanner;
                
                public class Loop {
                    public static void main(String[] args) {
                        Scanner leitura = new Scanner(System.in);
                        double mediaAvaliacao = 0;
                        double nota = 0;
                        
                        for (int i = 0; i < 3; i++) {
                            System.out.println("Diga sua avaliação para o filme  ");
                            nota = leitura.nextDouble();
                            mediaAvaliacao += nota;
                        }
                        
                        System.out.println("Média de avaliações " + mediaAvaliacao/3);
                        
                    }
                }
            

Utilizando While


                import java.util.Scanner;
                
                public class OutroLoop {
                    public static void main(String[] args) {
                        Scanner leitura = new Scanner(System.in);
                        double mediaAvaliacao = 0;
                        double nota = 0;
                        int totalDeNotas = 0;
                        
                        while (nota != -1) {
                        System.out.println("Diga sua avaliação para o filme ou -1 para encerrar  ");
                        nota = leitura.nextDouble();
                        
                        if (nota != -1) {
                            mediaAvaliacao +=  nota;
                            totalDeNotas++;
                        }
                        
                    }
                    
                    System.out.println("Média de avaliações " + mediaAvaliacao / totalDeNotas);
                }
            }
            

construtor padrão

Em Java, um construtor é um método especial usado para criar e inicializar um objeto recém-criado. Quando uma classe é definida, ela pode ter um ou mais construtores, sendo que se nenhum construtor for definido explicitamente, o Java criará um construtor default (padrão) automaticamente.

Um construtor default é um construtor que não possui parâmetros e não executa nenhuma instrução. Ele é chamado sempre que um objeto da classe é criado sem argumentos. Por exemplo:

                public class Pessoa {
                    
                    private String nome;
                    private String email;
                    
                    public Pessoa() {
                    }
                    
                    //metodos getters/setters
                }
            

No exemplo de código anterior, a classe Pessoa possui um construtor default, que será exatamente o mesmo construtor que o Java criará automaticamente, caso nenhum construtor tivesse sido definido na classe.

Se uma classe define explicitamente um ou mais construtores, mas não define um construtor sem parâmetros, então não há construtor default. Nesse caso, se um objeto é criado sem argumentos, um erro de compilação será gerado.

É importante ressaltar que mesmo que um construtor default possa ser útil em alguns casos, é sempre recomendável definir explicitamente os construtores da classe, especialmente se a classe tiver atributos que precisam ser inicializados com valores específicos ou obrigatórios. Isso também torna o código mais claro e fácil de entender.

Modificadores de Acesso

Em Java, os modificadores de acesso são palavras-chave que definem o nível de visibilidade de classes, atributos e métodos, sendo que eles ajudam a garantir a segurança e encapsulamento do código.

Existem quatro tipos de modificadores de acesso em Java: public, protected, private e default (também conhecido como package-private).

Public

O modificador de acesso public é o mais permissivo de todos. Uma classe, atributo ou método declarado como public pode ser acessado por qualquer classe em qualquer pacote. Ou seja, ele possui visibilidade pública e pode ser utilizado livremente. Por exemplo:

                public class Conta {

                    public double saldo;

                    public void sacar(double valor) {
                        // lógica de saque...
                    }
                }
            

Default (Package-private)

O modificador de acesso default é aquele que não especifica nenhum modificador de acesso. Quando nenhum modificador de acesso é especificado, a classe, atributo ou método pode ser acessado apenas pelas classes que estão no mesmo pacote. Por exemplo:

                package br.com.alura.conta;

                public class Conta {

                    double saldo;

                    void sacar(double valor) {
                        // lógica de saque...
                    }
                }
            
                package br.com.alura.testes;

                public class Principal {
                    
                    public static void main(String[] args) {
                        Conta c1 = new Conta();
                        c1.saldo = 300;
                        c1.sacar(100);
                    }

                }
            

No código anterior, a classe Conta está em um pacote e a classe Principal em outro pacote distinto. A classe Conta pode ser instanciada dentro da classe Principal, pois ela possui o modificador de acesso public, entretanto, o atributo saldo e o método sacar tem o modificador default e, portanto, não podem ser acessados de dentro da classe Principal, o que vai causar um erro de compilação no código anterior.

Private

O modificador de acesso private é o mais restritivo de todos. Uma classe, atributo ou método declarado como private só pode ser acessado dentro da própria classe. Ou seja, ele possui visibilidade restrita e não pode ser utilizado por outras classes. Por exemplo:

                public class Conta {

                    private double saldo;

                    private void sacar(double valor) {
                        // lógica de saque...
                    }
                }
            

Protected

Ao usar herança no Java, temos a possibilidade de utilizar o modificador de acesso protected, que permite que os atributos de uma classe sejam acessados por outras classes do mesmo pacote e também por suas subclasses, independentemente do pacote em que se encontram.

O modificador protected é útil em situações em que uma classe precisa permitir que suas subclasses acessem diretamente seus atributos, sem a necessidade de restringir o acesso apenas pelos métodos getters e setters. Por exemplo, suponha que temos as seguintes classes em um projeto:

                public class Conta {

                    private String titular;
                    private double saldo;

                    public void sacar(double valor) {
                        //implementacao do metodo omitida
                    }

                    public void depositar(double valor) {
                        //implementacao do metodo omitida
                    }

                    //getters e setters
                }
            
                public class ContaPoupanca extends Conta {

                    private double taxaDeJuros;

                    public void calcularJuros() {
                        double juros = this.getSaldo() * taxaDeJuros;
                        System.out.println("Juros atual: " +juros);
                    }

                    //getters e setters
                }
            

No código anterior, repare que no método calcularJuros, da classe ContaPoupanca, o atributo saldo não é acessado diretamente, pois ele foi declarado como private na classe Conta, devendo então seu acesso ser feito pelo método getSaldo().

Podemos declarar o atributo saldo como protected, para evitar essa situação e liberar o acesso direto a ele pelas classes que herdam da classe Conta:

                public class Conta {

                    private String titular;
                    protected double saldo;

                    public void sacar(double valor) {
                        //implementacao do metodo omitida
                    }

                    public void depositar(double valor) {
                        //implementacao do metodo omitida
                    }

                    //getters e setters
                }
            
                public class ContaPoupanca extends Conta {

                    private double taxaDeJuros;

                    public void calcularJuros() {
                        double juros = this.saldo * taxaDeJuros;
                        System.out.println("Juros atual: " +juros);
                    }

                    //getters e setters
                }
            

Repare que agora o atributo saldo foi acessado diretamente pela classe ContaPoupanca.

Herança

A herança é um conceito fundamental da orientação a objetos, sendo implementada em Java através da relação é um entre classes. Isso significa que uma classe pode herdar atributos e métodos de outra classe, tornando com isso o código mais reutilizável.

No Java, a herança é realizada através da palavra-chave extends. A classe que herda é chamada de subclasse, e a classe que é herdada é chamada de superclasse. A subclasse pode acessar todos os atributos e métodos públicos e protegidos da superclasse, além de poder sobrescrever os métodos da superclasse para criar comportamentos específicos.

Por exemplo:

                public class Conta {

                    private String titular;
                    private double saldo;

                    public void sacar(double valor) {
                        if (valor <= 0) {
                        System.out.println("Valor deve ser maior do que zero!");
                        } else if (saldo >= valor) {
                        saldo -= valor;
                        System.out.println("Saque realizado com sucesso. Saldo atual: " +saldo);
                        } else {
                        System.out.println("Saldo insuficiente.");
                        }
                    }

                    public void depositar(double valor) {
                        if (valor > 0) {
                        saldo += valor;
                        System.out.println("Depósito realizado com sucesso. Saldo atual: " +saldo);
                        } else {
                        System.out.println("Valor deve ser maior do que zero!");
                        }
                    }

                    //getters e setters
                }
            
                public class ContaPoupanca extends Conta {

                    private double taxaDeJuros;

                    public void calcularJuros() {
                        double juros = this.getSaldo() * taxaDeJuros;
                        System.out.println("Juros atual: " +juros);
                    }

                    public void sacar(double valor) {
                        double taxaSaque = 0.01;
                        super.sacar(valor + taxaSaque);
                    }

                    //getters e setters
                }
            

No código anterior, a classe Conta é a superclasse e a classe ContaPoupanca é a subclasse. A classe ContaPoupanca herda os atributos e métodos da classe Conta, e adiciona um novo atributo taxaDeJuros e um novo método calcularJuros. Embora os atributos sejam herdados, como eles foram declarados como private na superclasse, não poderão ser acessados diretamente na subclasse, devendo então serem utlizados os métodos getters/setter, que são públicos.

Repare também no código anterior que a subclasse sobrescreveu o método sacar, para que seja descontada a taxa de saque, além de utilizar a palavra chave super para chamar o método da superclasse, evitando com isso duplicar um código já existente. Essa é a grande vantagem da herança: reaproveitamento de código com flexibilidade para sobrescrever comportamentos.

Herança múltipla

Em Java, é importante notar que a herança múltipla não é permitida. A herança múltipla ocorre quando uma subclasse herda de duas ou mais superclasses. Por exemplo:

                public class ContaPoupanca extends Conta, Pagamento {
                    //codigo da classe omitido
                }
            

O código anterior não compila, pois o extends aceita apenas uma única classe, ou seja, uma classe pode ter apenas uma superclasse.

Entretanto, é possível criar uma hierarquia de classes utilizando herança, simulando com isso uma herança múltipla. Por exemplo:

                public class Conta {
                //codigo da classe omitido
                }
            
                public class ContaCorrente extends Conta {
                //codigo da classe omitido
                }
            
                public class ContaCorrentePessoaFisica extends ContaCorrente  {
                //codigo da classe omitido
                }
            

No código anterior, a classe ContaCorrentePessoaFisica está herdando de ContaCorrente, que por sua vez herda da classe Conta, ou seja, indiretamente a classe ContaCorrentePessoaFisica vai herdar de Conta, pois sua superclasse herda dela.

Interface

Em Java, interfaces são uma forma de definir um contrato que as classes devem seguir, sendo que ele define quais métodos devem ser implementados pelas classes que o implementarem. Interfaces permitem que diferentes classes possam ser tratadas de maneira padronizada, via polimorfismo, tornando assim o código fácil de estender com novos comportamentos.

No Java, uma interface é definida usando a palavra-chave interface. Por exemplo:

                public interface Tributavel {

                    double getValorImposto();

                }
            

No exemplo de código anterior, estamos definindo uma interface chamada Tributavel, sendo que ela possui apenas um método chamado getValorImposto() que retorna um valor do tipo double. Essa interface pode ser implementada por qualquer classe que queira ser tributável no projeto.

Para implementar uma interface, usamos a palavra-chave implements após a definição da classe. A classe que implementa a interface deve implementar todos os métodos definidos na interface. Por exemplo:

                public class Produto implements Tributavel {

                    private String nome;
                    private double valor;

                    @Override
                    public double getValorImposto() {
                        return this.valor * 0.1;
                    }

                    //getters e setters
                }
            

No exemplo anterior, estamos criando uma classe chamada Produto que implementa a interface Tributavel. Essa classe implementa o método getValorImposto(), que está definido na interface Tributavel, com uma lógica de que o imposto do produto é igual a 10% do seu valor.

Também poderíamos ter uma classe de serviços, conforme abaixo:

                public class Servico implements Tributavel {

                    private String descricao;
                    private double valor;
                    private double aliquotaISS;

                    @Override
                    public double getValorImposto() {
                        return this.valor * this.aliquotaISS / 100;
                    }

                    //getters e setters
                }
            

No exemplo acima, estamos criando uma classe chamada Servico que implementa a interface Tributavel. Essa classe implementa o método getValorImposto(), que está definido na interface Tributavel, com uma lógica de que o imposto do serviço é igual ao seu valor vezes a alíquota de ISS definida e dividido por 100. Então para um serviço de R$ 1.200,00 e alíquota de 5%, o método retornará: 1200 * 5 / 100, cujo valor do imposto fica R$ 60,00.

Utilização de interfaces

Interfaces podem ser utilizadas para definir comportamentos que podem ser aplicados a várias classes diferentes, tornando assim o código mais modular e fácil de manter.

Por exemplo, suponha que temos um sistema de vendas que precisa calcular o imposto de diferentes tipos de produtos. Podemos criar a interface Tributavel, para definir o comportamento de calcular imposto, e criar várias classes diferentes que implementam essa interface para calcular o imposto de diferentes produtos. Por exemplo:

                public class CalculadoraImposto {

                    private double totalImposto = 0;

                    public void calcularImposto(Tributavel item) {
                        this.totalImposto += item.getValorImposto();
                    }

                    public double getTotalImposto() {
                    return this.totalImposto;
                    }

                }
            

Nesse exemplo, estamos criando uma classe chamada CalculadoraImposto, que tem um atributo privado chamado totalImposto, que armazena o valor total dos impostos.

Repare que o método calcularImposto recebe um parâmetro do tipo Tributavel. Ao declarar uma variável com o tipo de uma interface, como é feito nesse método, podemos atribuir a essa variável qualquer objeto que implemente essa interface, ou seja, tanto um objeto do tipo Servico quanto Produto. Para ambos os casos, a CalculadoraImposto irá chamar o método implementado na classe específica. Ou seja, para um produto, irá chamar o método getTotalImposto implementado na classe Produto. E para um serviço, irá chamar o método getTotalImposto implementado na classe Servico.

Isso é muito útil quando queremos tratar vários objetos de classes diferentes de forma semelhante, permitindo que diferentes classes possam ser tratadas de maneira padronizada, facilitando a manutenção e extensão do código. Esse é mais um exemplo de aplicação do polimorfismo em Java, mas agora com a utilização de interfaces.

Declarando Variáveis com var

A partir da versão 10 do Java, foi adicionada uma nova funcionalidade para a declaração de variáveis chamada var. Essa nova palavra-chave permite que o compilador infira automaticamente o tipo da variável com base no valor atribuído a ela. Isso pode tornar o código mais limpo e legível, além de reduzir a digitação de código redundante.

Sintaxe básica

A sintaxe básica para declarar uma variável com var é a seguinte:

var nomeDaVariavel = valorInicial;

Onde nomeDaVariavel é o nome que você quer dar à variável e valorInicial é o valor que você quer atribuir a ela. O tipo da variável será inferido automaticamente pelo compilador com base no valor atribuído.

Exemplo:

var numero = 10;

Nesse exemplo, a variável numero será inferida como sendo do tipo int, já que o valor atribuído é um número inteiro.

Limitações

A declaração de variáveis com var possui algumas limitações:

O tipo da variável deve ser inferido automaticamente pelo compilador. Isso significa que não é possível utilizar var em variáveis cujo tipo não possa ser inferido automaticamente.

Não é possível usar var em variáveis sem valor inicial. É necessário atribuir um valor à variável na mesma linha em que ela é declarada.

Array

Em Java, arrays são estruturas de dados que permitem armazenar uma coleção de elementos do mesmo tipo. Eles são muito utilizados para manipulação de dados em projetos de programação.

Para declarar um array em Java, é preciso definir seu tipo e tamanho. Por exemplo, para criar um array de inteiros com tamanho 5, podemos escrever o seguinte código:

int[] numeros = new int[5];

Aqui, estamos declarando um array chamado "numeros" do tipo "int" e com tamanho 5. É importante lembrar que o índice dos elementos de um array começa em 0 e vai até o tamanho do array menos 1.

Após declarar um array, podemos inicializá-lo com valores. Por exemplo, podemos preencher o array "numeros" com os números de 1 a 5 da seguinte forma:


                for (int i = 0; i < numeros.length; i++) {
                    numeros[i] = i + 1;
                }
                
            

Aqui, estamos percorrendo o array "numeros" utilizando um loop for e preenchendo cada posição com seu respectivo índice mais 1.

Também é possível criar arrays de objetos e não apenas de tipos primitivos. Por exemplo:


                Filme[] filmes = new Filme[2];
                
                Filme filme1 = new Filme("Avatar", 2009);
                Filme filme2 = new Filme("Dogville", 2003);
                
                filmes[0] = filme1;
                filmes[1] = filme2;
                
            

Embora os arrays sejam úteis, eles possuem algumas limitações que podem causar problemas em projetos. Alguns desses problemas incluem:

Tamanho fixo: o tamanho de um array é fixo e não pode ser alterado após a sua criação. Isso pode ser problemático em situações em que o tamanho dos dados a serem armazenados é desconhecido ou variável.

Ausência de métodos: arrays não possuem métodos que permitam a inserção, remoção ou pesquisa de elementos de forma eficiente. Isso pode levar a soluções de código complicadas e ineficientes para tarefas simples.

Justamente por conta desses problemas e dificuldades é que não devemos utilizar arrays para representar uma coleção de elementos, mas sim alguma classe do Java, como a ArrayList, que encapsula e abstrai um array, facilitando a sua utilização via métodos e deixando o código do projeto mais simples de entender e evoluir.

ArrayList

Criando ArrayList

ArrayList<Filme> listaDeFilmes = new ArrayList<>();

Adicionando

listaDeFilmes.add(filmeDoPaulo);

Tamando da Lista

listaDeFilmes.size();

Acessando Objeto

listaDeFilmes.get(0).getNome();

Arrays no Java

Em Java, arrays são estruturas de dados que permitem armazenar uma coleção de elementos do mesmo tipo. Eles são muito utilizados para manipulação de dados em projetos de programação.

Para declarar um array em Java, é preciso definir seu tipo e tamanho. Por exemplo, para criar um array de inteiros com tamanho 5, podemos escrever o seguinte código:

int[] numeros = new int[5];

Aqui, estamos declarando um array chamado "numeros" do tipo "int" e com tamanho 5. É importante lembrar que o índice dos elementos de um array começa em 0 e vai até o tamanho do array menos 1.

Após declarar um array, podemos inicializá-lo com valores. Por exemplo, podemos preencher o array "numeros" com os números de 1 a 5 da seguinte forma:

                    for (int i = 0; i < numeros.length; i++) {
                        numeros[i] = i + 1;
                    }
            

Aqui, estamos percorrendo o array "numeros" utilizando um loop for e preenchendo cada posição com seu respectivo índice mais 1.

Também é possível criar arrays de objetos e não apenas de tipos primitivos. Por exemplo:

                Filme[] filmes = new Filme[2];

                Filme filme1 = new Filme("Avatar", 2009);
                Filme filme2 = new Filme("Dogville", 2003);

                filmes[0] = filme1;
                filmes[1] = filme2;
            

Embora os arrays sejam úteis, eles possuem algumas limitações que podem causar problemas em projetos. Alguns desses problemas incluem:

Tamanho fixo: o tamanho de um array é fixo e não pode ser alterado após a sua criação. Isso pode ser problemático em situações em que o tamanho dos dados a serem armazenados é desconhecido ou variável.

Ausência de métodos: arrays não possuem métodos que permitam a inserção, remoção ou pesquisa de elementos de forma eficiente. Isso pode levar a soluções de código complicadas e ineficientes para tarefas simples.

Justamente por conta desses problemas e dificuldades é que não devemos utilizar arrays para representar uma coleção de elementos, mas sim alguma classe do Java, como a ArrayList, que encapsula e abstrai um array, facilitando a sua utilização via métodos e deixando o código do projeto mais simples de entender e evoluir.

Construtor

Em Java, um construtor é um método especial usado para criar e inicializar um objeto recém-criado. Quando uma classe é definida, ela pode ter um ou mais construtores, sendo que se nenhum construtor for definido explicitamente, o Java criará um construtor default (padrão) automaticamente.

Um construtor default é um construtor que não possui parâmetros e não executa nenhuma instrução. Ele é chamado sempre que um objeto da classe é criado sem argumentos. Por exemplo:

                public class Pessoa {

                    private String nome;
                    private String email;

                    public Pessoa() {
                    }

                    //metodos getters/setters
                }
            

No exemplo de código anterior, a classe Pessoa possui um construtor default, que será exatamente o mesmo construtor que o Java criará automaticamente, caso nenhum construtor tivesse sido definido na classe.

Se uma classe define explicitamente um ou mais construtores, mas não define um construtor sem parâmetros, então não há construtor default. Nesse caso, se um objeto é criado sem argumentos, um erro de compilação será gerado.

É importante ressaltar que mesmo que um construtor default possa ser útil em alguns casos, é sempre recomendável definir explicitamente os construtores da classe, especialmente se a classe tiver atributos que precisam ser inicializados com valores específicos ou obrigatórios. Isso também torna o código mais claro e fácil de entender.

Formas de Percorrer Listas

A forma mais comum de percorrer uma lista no Java é utilizando o laço foreach tradicional, também conhecido como for-each. Esse laço permite que se percorra todos os elementos de uma lista, sem a necessidade de se preocupar com índices ou o tamanho dela, tornando o código mais simples e legível. Por exemplo, suponha que tenhamos uma lista de nomes de pessoas e que desejamos imprimi-los na tela:

                ArrayList<String> nomes = new ArrayList<>();
                nomes.add("Jacqueline");
                nomes.add("Paulo");
                nomes.add("Suellen");
                nomes.add("Emily");

                for (String nome : nomes) {
                    System.out.println(nome);
                }                
            

Esse loop for percorre todos os elementos da lista, atribuindo cada um deles à variável nome, que é usada para imprimir o valor na tela. Esse tipo de loop é muito útil em situações onde não precisamos realizar nenhuma operação complexa sobre os elementos da lista.

No entanto, a partir do Java 8, foi adicionado na interface List, a qual a classe ArrayList implementa, um novo método chamado forEach, que possibilita a iteração sobre os elementos da lista de forma mais concisa e elegante. Por exemplo, o exemplo anterior pode ser reescrito utilizando o método forEach da seguinte forma:

nomes.forEach(nome -> System.out.println(nome));

Nesse caso, o método forEach é chamado sobre a lista nomes e recebe como parâmetro uma expressão lambda que realiza a impressão do valor na tela. A expressão lambda nome -> System.out.println(nome) é uma forma compacta de definir uma função que recebe um parâmetro nome e realiza a operação de impressão.

É possível simplificar ainda mais o exemplo de código anterior, utilizando o recurso conhecido como Method Reference, que nada mais é do que uma forma reduzida de uma expressão lambda:

nomes.forEach(System.out::println);

No código anterior, o símbolo :: é a sintaxe do Method Reference, que no exemplo mostrado faz uma referência para o método println.

Variáveis e referências

Referências são ponteiros para objetos em memória, ou seja, elas apontam para um objeto e permitem que você trabalhe com ele. No Java, toda variável de objeto é na verdade uma referência a esse objeto que foi alocado na memória.

Quando você instancia um objeto, está, na realidade, criando um novo bloco de memória que armazena as informações desse objeto. A maneira de chegar a esse bloco de memória, para armazenar e ler informações dele, ocorre por meio de uma referência, que é representada por uma variável. Por exemplo:

Filme filme1 = new Filme("Avatar", 2009);

No exemplo de código anterior, criamos um novo objeto da classe Filme e armazenamos uma referência a ele na variável filme1.

É importante lembrar que as referências a objetos em Java não são o próprio objeto em si, pois elas apenas apontam para o objeto. Quando você passa uma referência a um método ou atribui uma referência a outra variável, está apenas copiando o valor da referência e não do objeto em si. Por exemplo:

                Filme filme1 = new Filme("Avatar", 2009);
                Filme filme2 = new Filme("The Matrix", 1999);
                Filme filme3 = filme1;
            

No exemplo de código anterior, foram criados apenas dois objetos em memória. A variável filme3 é apenas uma referência que aponta para o mesmo objeto sendo referenciado pela variável filme1.

Uma questão importante relacionada com referências a objetos em Java é a questão da igualdade e identidade de objetos. Quando você compara duas referências de objeto usando o operador de igualdade ==, está comparando as referências em si, não os objetos que elas apontam. Por exemplo:

                Filme filme1 = new Filme("Avatar", 2009);
                Filme filme2 = new Filme("Avatar", 2009);

                if (filme1 == filme2) {
                    System.out.println("Iguais");
                } else {
                    System.out.println("Diferentes");
                }
            

No exemplo de código anterior, a saída no console será: "Diferentes". Embora os dois objetos tenham as mesmas informações na memória, a comparação com == verifica se as referências são iguais, ou seja, se apontam para o mesmo objeto na memória.

Utilizando Collections.sort para ordenar listas

                ArrayList buscaPorArtista = new ArrayList<>();
                buscaPorArtista.add("Adam Sandler");
                buscaPorArtista.add("Paulo");
                buscaPorArtista.add("Jacqueline");
                System.out.println(buscaPorArtista);

                Collections.sort(buscaPorArtista);
                System.out.println("Depois da ordenação");
                System.out.println(buscaPorArtista);
            

Para utilizar o Collections.sort a classe precisa inplementar a interface Comparable no caso das variáveis primitivas e o objeto String as classes já implementam esta interface, porém as classes que não tiverem esta interface, precisamos implementa-la.

Implementando a interface Comparable

Primeiro instanciamos a interface Comparable na classe

public class Titulo implements Comparable<Titulo>{...}

Depois precisamos criar o método da interface que é o compareTo

                @Override
                public int compareTo(Titulo outroTitulo) {
                    return this.getNome().compareTo(outroTitulo.getNome());
                }
            

Agora podemos utilizar o Collections.sort em nossa lista de objetos

Collections.sort(lista);

Se quisermos ordenar utilizando outros métodos de comparação:

lista.sort(Comparator.comparing(Titulo::getAnoDeLancamento));

Outras classes de listas no Java

O Java oferece diferentes classes para representar uma lista de objetos. Essas classes são úteis em diferentes cenários, dependendo das necessidades de cada aplicação.

As classes mais comuns para representar uma lista no Java são:

ArrayList

A principal característica do ArrayList é que ele é baseado em um array dinâmico. Ele armazena os elementos em uma matriz interna e, conforme novos elementos são adicionados, o tamanho da matriz é automaticamente ajustado para acomodar o novo elemento. Da mesma forma, quando um elemento é removido, o tamanho do array é ajustado para evitar o desperdício de espaço. O ArrayList é amplamente utilizado devido à sua facilidade de uso e eficiência em termos de desempenho.

LinkedList

A classe LinkedList fornece uma lista encadeada de elementos. Diferentemente do ArrayList, que é baseado em um array, o LinkedList é baseado em uma lista encadeada, o que significa que cada elemento da lista é um objeto que contém uma referência para o próximo elemento. Isso permite que os elementos sejam adicionados e removidos de maneira eficiente em qualquer posição da lista, mas pode tornar a pesquisa de um elemento específico menos eficiente./p>

O LinkedList é uma boa escolha quando a inserção e remoção de elementos em qualquer posição da lista é frequente e quando não é necessário acessar os elementos de forma aleatória.

Vector

A classe Vector é semelhante ao ArrayList, mas é sincronizada, o que significa que é segura para uso em threads concorrentes. No entanto, a sincronização adiciona uma sobrecarga de desempenho, então o Vector pode ser mais lento que o ArrayList em algumas situações.

Stack

A classe Stack implementa uma pilha, que é uma coleção ordenada de elementos onde a inserção e remoção de elementos ocorrem sempre no mesmo extremo da lista. Os elementos são adicionados e removidos em uma ordem conhecida como "last-in, first-out" (LIFO), ou seja, o último elemento adicionado é o primeiro a ser removido. A classe Stack é usada com frequência em algoritmos de processamento de texto, bem como em outras situações em que a LIFO é a maneira natural de organizar os dados.

Cada uma dessas classes tem seus próprios pontos fortes e fracos, e a escolha de qual usar dependerá das necessidades específicas da aplicação. Para um melhor entendimento sobre estruturas de dados, recomendamos a leitura dos seguintes artigos:

Map e HashMap

Uma das características mais importantes do Java é sua vasta biblioteca padrão, que oferece muitas classes e interfaces úteis para os desenvolvedores. Entre elas, estão o Map e o HashMap, que são ferramentas essenciais para associação de chaves e valores em muitas aplicações Java.

Map

O Map é uma interface que permite que os desenvolvedores associem chaves a valores. É uma estrutura de dados útil para muitas aplicações Java, especialmente aquelas que envolvem a manipulação de grandes quantidades de dados, portanto, é comum usá-lo para realizar buscas, atualização e recuperação de elementos por chaves

Ele é implementado por diversas classes, sendo a mais comum delas o HashMap.

HashMap

O HashMap é uma classe que implementa a interface Map usando uma tabela hash para armazenar os pares chave-valor. Ele é conhecido por sua eficiência em termos de tempo de execução. Essa classe tem uma complexidade de tempo O(1) - constante - para inserção, recuperação e remoção de elementos. Isso significa que o desempenho do HashMap não depende do tamanho da coleção de dados!

No entanto, é importante lembrar que o HashMap não mantém a ordem de inserção dos elementos e não garante a ordem dos elementos na saída. Isso ocorre porque a ordem dos elementos depende da função de hash usada para mapear as chaves para índices na tabela hash. Além disso, o desempenho do HashMap pode ser afetado se houver muitas colisões de hash entre as chaves.

Por exemplo:

                import java.util.HashMap;
                import java.util.Map;

                public class ExemploHashMap {

                    public static void main(String[] args) {
                        //Criando um objeto da classe HashMap que implementa a interface Map
                        Map usandoHashMap = new HashMap<>();

                        // Adicionando pares chave-valor
                        usandoHashMap.put("Gatos", 1);
                        usandoHashMap.put("Cachorros", 2);
                        usandoHashMap.put("Roedores", 3);

                        // Acessando um valor através de uma chave
                        int valor = usandoHashMap.get("Cachorros");
                        System.out.println("Valor da chave Cachorros: " + valor);

                        // Removendo um par chave-valor
                        usandoHashMap.remove("Gatos");

                        // Iterando sobre as chaves
                        for (String chave : usandoHashMap.keySet()) {
                            System.out.println("Chave: " + chave);
                            System.out.println("Valor: " + usandoHashMap.get(chave));
                        }
                    }
                }
            

O resultado será:

                Valor da chave Cachorros: 2
                Chave: Cachorros
                Valor: 2
                Chave: Roedores
                Valor: 3
            

HttpRequest

                HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create("https://viacep.com.br/ws/37490000/json/"))
                    .build();

                HttpResponse<String> response = HttpClient
                        .newHttpClient()
                        .send(request, HttpResponse.BodyHandlers.ofString());

                String resposta = response.body();
            

Adicionando Biblioteca manualmente

Primeiro acessar o site MVN Repository https://mvnrepository.com/

Pesquisar Gson

Acessar a biblioteca Gson

Selecionar uma versão sem vulnerabilidade

Clicar em Jar que esta em files, para baixar o arquivo jar

Agora no IntelliJ

Project Structure
clicar em file e depois em Project Structure
Dependencies
Ir em Project Settings > Modules > Dependencies
Adicionando Arquivo
Clicar em + para adicionar o arquivo e depois em ok

Agora basta colocar:

Gson gson = new Gson();

Classe Record

Utilizado para Tradução de campos, quando precisamos receber dados de uma api e traduzir para nossa classe

Classe do tipo Record

Clicar em cima da pasta modelos com o botão direito

New Java Class

Escolhe o nome e depois seleciona Record

Agora apenas precisamos definir os atributos que desejamos, de acordo com os atributos que vem da api

public record TituloOmdb(String title, String year, String runtime) {}

                String json = response.body();
                System.out.println(json);

                Gson gson = new Gson();

                TituloOmdb meuTituloOmdb = gson.fromJson(json, TituloOmdb.class);
                System.out.println(meuTituloOmdb);
            

A saída será:

                {"Title":"Matrix","Year":"1993","Rated":"N/A","Released":"01 Mar 1993","Runtime":"60 min","Genre":"Action, Drama, Fantasy","Director":"N/A","Writer":"Grenville Case","Actors":"Nick Mancuso, Phillip Jarrett, Carrie-Anne Moss","Plot":"Hitman Steven Matrix is shot, experiences afterlife, gets second chance by helping others. Wakes up, meets guides assigning cases where he aids people using unorthodox methods from past profession.","Language":"English","Country":"Canada","Awards":"1 win total","Poster":"https://m.media-amazon.com/images/M/MV5BM2JiZjU1NmQtNjg1Ni00NjA3LTk2MjMtNjYxMTgxODY0NjRhXkEyXkFqcGc@._V1_SX300.jpg","Ratings":[{"Source":"Internet Movie Database","Value":"7.2/10"}],"Metascore":"N/A","imdbRating":"7.2","imdbVotes":"225","imdbID":"tt0106062","Type":"series","totalSeasons":"N/A","Response":"True"}
                TituloOmdb[title=null, year=null, runtime=null]
            

title year e runtime aparecem como null, pois na api as propriedades estão em maiúsculo, porém o padrão para variáveis é em minúsculo.

Na biblioteca Gson já temos uma opção para contornar este problema.

Pesquisamos Field Name Policy na documentação e acharemos um código para resolvermos o problema:

Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();

Agora Utilizando a classe Record para podermos usar a nossa classe

Titulo meuTitulo = new Titulo(meuTituloOmdb);

Agora precisamos criar um construtor na classe Titulo que recebe TituloOmdb

                public Titulo(TituloOmdb meuTituloOmdb) {
                    this.nome = meuTituloOmdb.title();
                    this.anoDeLancamento = Integer.valueOf(meuTituloOmdb.year());
                    this.duracaoEmMinutos = Integer.valueOf(meuTituloOmdb.runtime().split(" ")[0]);
                }
            

Como a resposta json vem em String, precisamos converter para os tipos da nossa classe.

No caso do nome, não foi necessário, porém no caso do anoDeLancamento e duracaoEmMinutos já foi necessário.

Para saber mais: Java Record

Lançado oficialmente no Java 16, mas disponível desde o Java 14 de maneira experimental, o Record é um recurso que permite representar uma classe imutável, contendo apenas atributos, construtor e métodos de leitura, de uma maneira muito simples e enxuta.

Esse recurso se encaixa perfeitamente quando precisamos criar um objeto apenas para representar dados, sem nenhum tipo de comportamento.

Para se criar uma classe imutável, sem a utilização do Record, era necessário escrever muito código. Vejamos um exemplo de uma classe que representa um telefone:

                public final class Telefone {

                    private final String ddd;
                    private final String numero;

                    public Telefone(String ddd, String numero) {
                        this.ddd = ddd;
                        this.numero = numero;
                    }

                    @Override
                    public int hashCode() {
                        return Objects.hash(ddd, numero);
                    }

                    @Override
                    public boolean equals(Object obj) {
                        if (this == obj) {
                            return true;
                        } else if (!(obj instanceof Telefone)) {
                            return false;
                        } else {
                            Telefone other = (Telefone) obj;
                            return Objects.equals(ddd, other.ddd)
                            && Objects.equals(numero, other.numero);
                        }
                    }

                    public String getDdd() {
                        return this.ddd;
                    }

                    public String getNumero() {
                        return this.numero;
                    }
                }
            

Agora com o Record, todo esse código pode ser resumido com uma única linha:

public record Telefone(String ddd, String numero){}

Por baixo dos panos, o Java vai transformar esse Record em uma classe imutável, muito similar ao código exibido anteriormente.

Bibliotecas e Frameworks

Bibliotecas e frameworks em Java, e também em outras linguagens de programação, são ferramentas essenciais para quem trabalha com programação, pois ajudam a reduzir a quantidade de trabalho necessária para construir aplicações, uma vez que fornecem funcionalidades prontas para uso, permitindo que as pessoas desenvolvedoras foquem na lógica de negócios dos projetos, ao invés de se preocuparem com problemas técnicos que são comuns e já possuem soluções prontas para uso.

Em Java, bibliotecas são coleções de classes e interfaces que oferecem uma série de recursos e funcionalidades prontas para uso. Geralmente elas são distribuídas como arquivos JAR (Java Archive), que são pacotes de arquivos Java que contém classes e outros recursos, como imagens e arquivos de configuração. As bibliotecas podem ser importadas em projetos Java e usadas diretamente em código para implementar funcionalidades específicas, como manipulação de arquivos, conexão com bancos de dados, criptografia, etc.

Frameworks, por outro lado, são estruturas de software que fornecem uma arquitetura básica para o desenvolvimento de aplicações. Eles incluem bibliotecas, padrões e práticas recomendadas para orientar o processo de desenvolvimento de aplicações. Um framework pode ser considerado como uma "fábrica padronizada de aplicações", que fornece os componentes necessários para criar uma aplicação, bem como um conjunto de regras e diretrizes para guiá-lo no processo.

Existem muitos frameworks populares em Java, cada um com suas próprias características e objetivos. Alguns exemplos incluem o Spring Framework, que é um framework que facilita a criação de aplicações Web e APIs Rest complexas em Java; o Hibernate, que é um framework de mapeamento objeto-relacional e simplifica muito o processo de integração de uma aplicação Java com um banco de dados relacional.

Para saber mais: imutabilidade

A imutabilidade, citada anteriormente ao falarmos sobre record, é um conceito importante em Java, que se refere à capacidade de um objeto não poder ser alterado depois de criado. Existem algumas classes que são imutáveis por padrão, como por exemplo, as classes String, Integer, Boolean, entre outras. Isso significa que, uma vez criado um objeto dessas classes, não é possível modificar o seu estado.

Vamos exemplificar. Dado o record abaixo:

public record Estudante(String nome, int idade) {}

Uma vez criado um objeto Estudante, seus valores não podem ser modificados:

Estudante estudante1 = new Estudante(“Alice”, 19);

Observe que após essa criação, eu não consigo setar outro nome ou idade para o objeto estudante1.

                estudante1.setNome(“Maria”); //Essa possibilidade não existe
                estudante1.nome = “Maria”; //Essa possibilidade não existe
            

Qualquer uma das tentativas acima, vai apresentar erro de compilação, pois não é possível atribuir nenhum outro nome a variável estudante1.

Com relação ao record, fica bem claro, certo? Mas e a String, por exemplo? Eu consigo fazer os passos abaixo no código:

                String nome = “Maria”;
                nome = “Alice”;
            

Se a String é imutável, o certo era eu não conseguir atribuir o conteúdo “Alice” à variável nome, correto?

No caso da String e de outras classes imutáveis que citei acima, a variável nome contém uma referência ao objeto da classe String que contém o valor "Maria".

No entanto, quando você tenta alterar o valor da string, o que realmente acontece é que um novo objeto da classe String é criado com o novo valor e a variável é atualizada para armazenar uma referência ao novo objeto.

Por isso, podemos dizer que a classe String é imutável, porque uma vez que um objeto da classe String é criado, ele não pode ser alterado. No entanto, as variáveis que armazenam referências a objetos da classe String podem ser atualizadas para referenciar novos objetos, que são criados a partir do conteúdo do objeto original.

A imutabilidade é importante por várias razões, entre elas:

Para saber mais: o bloco finally

Aprendemos que quando ocorre uma exceção, o Java permite tratar o erro usando a declaração try-catch. Entretanto, existe ainda o bloco finally, que é opcional, mas pode ser útil em certas situações.

O finally é usado para executar um bloco de código independentemente de ocorrer uma exceção ou não, ou seja, ele sempre é executado. Isso pode ser útil quando precisamos executar um código tanto no try, caso não ocorra uma exceção, quanto no catch, caso uma exceção seja lançada. Por exemplo, suponha que você tenha o seguinte código:

                try {
                    metodoQuePodeLancarExcecao();
                    System.out.println("Executou");

                    System.out.println("Finalizou!");
                    } catch (Exception e) {
                    System.out.println("Deu erro!");

                    System.out.println("Finalizou!");
                }
            

Perceba no código anterior que a instrução System.out.println("Finalizou!"); deve ser sempre executada, independente de ter acontecido ou exception ou não. Mas o problema é que ela acabou tendo de ser duplicada tanto no try quanto no catch. O bloco finally nos ajuda justamente a evitar essa duplicação de código:

                try {
                    metodoQuePodeLancarExcecao();
                    System.out.println("Executou");
                    } catch (Exception e) {
                    System.out.println("Deu erro!");
                    } finally {
                    System.out.println("Finalizou!");
                }
            

Repare que agora a instrução aparece apenas uma vez, dentro do bloco finally, evitando com isso uma duplicação de código desnecessária.

O finally é muito utilizado em situações onde é necessário limpar recursos, fechar conexões de banco de dados ou fechar arquivos que foram abertos no bloco try.

Para saber mais: hierarquia de exceptions no Java

No Java, as exceções são organizadas em uma hierarquia de classes. Todas as exceções são subclasses da classe Throwable, sendo que ela possui duas subclasses principais: Exception e Error.

hierarquia de exceptions no Java

As exceções que herdam da classe Exception são chamadas de exceções verificadas (checked exceptions). Isso significa que essas exceções devem ser tratadas explicitamente em um bloco try-catch ou declaradas em uma cláusula throws na assinatura do método. Um exemplo é a classe de exceção IOException, que indica algum problema relacionado com leitura/escrita de dados.

As exceções que herdam da classe Error representam erros irrecuperáveis pelo sistema, como falta de memória ou falhas internas. Um exemplo é a classe de exceção OutOfMemoryError, que indica que o Java não conseguiu memória suficiente do sistema operacional para executar corretamente a aplicação.

Além disso, existe ainda a classe de exceção RuntimeException, que é uma subclasse direta de Exception, e as classes que herdam dela são chamadas de exceções não verificadas (unchecked exception). As exceções não verificadas indicam erros lógicos no código, como a NullPointerException, que indica o acesso a algum atributo ou método de um objeto que é nulo, ou seja, que não foi instanciado ou foi atributo ao valor null.

Ao lidar com exceções em um bloco try-catch, é importante considerar a hierarquia de exceções. É possível capturar exceções de uma classe mãe em um bloco catch que captura exceções de uma classe filha. No entanto, o inverso não é possível. Isso significa que, se um bloco catch captura exceções de uma classe filha, ele não será capaz de capturar exceções de uma classe mãe.

Exemplo: Imagine uma exceção IOException (classe mãe) e uma exceção FileNotFoundException (classe filha). Um bloco catch que captura IOException irá capturar tanto IOException quanto FileNotFoundException, pois FileNotFoundException é um tipo específico de IOException. No entanto, um bloco catch que captura FileNotFoundException não irá capturar IOException.

É importante lembrar que, ao usar a hierarquia de classes para tratar exceções, devemos priorizar o tratamento específico de exceções de classes filhas. Em seguida, podemos incluir um bloco catch mais genérico para tratar exceções de classes mães.

Para saber mais: Multi-catch

A partir do Java 7, a linguagem introduziu uma nova funcionalidade chamada "multi-catch", que permite capturar várias exceções em um único bloco catch. Essa funcionalidade pode tornar o código mais conciso e legível, reduzindo a repetição de código.

O uso de multi-catch é muito simples. Em vez de ter vários blocos catch para lidar com diferentes exceções, você pode agrupá-las em um único bloco usando o caractere | para separar as exceções. Por exemplo, suponha que você tenha escrito o seguinte código:

                try {
                    metodoQuePodeLancarExcecao();
                } catch (NumberFormatException e) {
                    System.out.println("tratando erro...");
                } catch (IllegalArgumentException e) {
                    System.out.println("tratando erro...");
                }
            

Como o tratamento do erro é o mesmo para ambas as exceções, o código anterior poderia ter sido escrito utilizando o multi-catch:

                try {
                    metodoQuePodeLancarExcecao();
                } catch (NullPointerException | IllegalArgumentException e) {
                    System.out.println("tratando erro...");
                }
            

No exemplo anterior, estamos lidando com duas exceções diferentes: NullPointerException e IllegalArgumentException. Se qualquer uma dessas exceções for lançada dentro do bloco try, o mesmo bloco catch será executado.

Uma observação importante de lembrar, é que o uso de multi-catch só é permitido para exceções que não estão relacionadas por uma hierarquia de herança. Se duas exceções compartilham uma hierarquia de herança, você deve lidar com elas em blocos catch separados.

Crianco Exceção Personalizada

throw new ErroDeConvercaoDeAnoExeption("Não consegui converter o ano porque tem mais de 4 caracteres.");

A IDE irá mostrar um erro, pois não temos a classe ErroDeConvercaoDeAnoExeption, então é só passar o mause em cima que já da a opção de criar a classe.

Classe ErroDeConvercaoDeAnoExeption:

                public class ErroDeConvercaoDeAnoExeption extends RuntimeException {
                    private String mensagem;
                    public ErroDeConvercaoDeAnoExeption(String mensagem) {
                        this.mensagem = mensagem;
                    }

                    @Override
                    public String getMessage() {
                        return this.mensagem;
                    }
                }
            

Dentro do construtor da classe Titulo colocamos um if:

                public Titulo(TituloOmdb meuTituloOmdb) {
                    this.nome = meuTituloOmdb.title();

                    if(meuTituloOmdb.year().length() > 4){
                        throw new ErroDeConvercaoDeAnoExeption("Não consegui converter o ano porque tem mais de 4 caracteres.");
                    }
                    this.anoDeLancamento = Integer.valueOf(meuTituloOmdb.year());
                    this.duracaoEmMinutos = Integer.valueOf(meuTituloOmdb.runtime().split(" ")[0]);
                }
            

Nosso cath fica assim:

                catch (ErroDeConvercaoDeAnoExeption e){
                    System.out.println(e.getMessage());
                }
            

Para saber mais: o pacote java.io

O Java possui um pacote chamado java.io, que é um dos pacotes mais importantes da linguagem, pois fornece classes e interfaces para entrada e saída de dados em vários formatos, como arquivos, rede, teclado, dentre outros. Vamos conhecer as principais classes desse pacote.

A classe File

A classe File representa um arquivo ou diretório no sistema de arquivos do computador, permitindo que você crie, delete, liste e manipule arquivos e diretórios. Para criar um objeto File, você precisa passar o caminho do arquivo ou diretório como argumento para o construtor. Por exemplo:

File file = new File("C:\\meuArquivo.txt");

No código anterior, foi criado um objeto File que aponta para o arquivo "meuArquivo.txt" localizado na raiz do disco C:.

A classe File tem vários métodos úteis para interagir com arquivos e diretórios, como exists(), canRead(), canWrite(), isDirectory(), isFile(), mkdir() e delete().

As classes FileReader e FileWriter

As classes FileReader e FileWriter são usadas para ler e escrever dados em arquivos de texto, sendo que a classe FileReader lê os caracteres de um arquivo de texto, enquanto a classe FileWriter escreve os caracteres.

Para usar a classe FileReader, você precisa criar um objeto passando um objeto File que deseja ler como argumento. Em seguida, você pode ler os dados do arquivo usando o método read() ou read(char[]). Por exemplo:

                File file = new File("C:\\meuArquivo.txt");
                FileReader reader = new FileReader(file);

                int data = reader.read();
                while (data != -1) {
                    System.out.print((char) data);
                    data = reader.read();
                }
                reader.close();
            

No código anterior, é feita a leitura do conteúdo do arquivo "meuArquivo.txt" e seu conteúdo é impresso no console

Já a classe FileWriter segue o mesmo processo, porém fazendo o caminho inverso, ou seja, escrevendo caracteres no arquivo. Por exemplo:

                File file = new File("C:\\saida.txt");
                FileWriter writer = new FileWriter(file);
                writer.write("Olá, mundo!");
                writer.close();
            

No código anterior, é escrito uma mensagem no arquivo chamado "saida.txt".

O pacote java.io também fornece outras classes úteis, como:

Conversão de tipos

De double para int:

                double valorDouble = 19.5; 
                int valorInt = (int) valorDouble;
            

Pontos de atenção:

Exemplo de Código

valor.equalsIgnoreCase("v")Verifica se a String é igual ao valor passado ignorando maiúsculo e minúsculo.

Data e Hora com Java

Utilizado API java.time

Classes mais utilizadas:

Métodos mais utilizados:

Exemplos

import java.time.LocalDate;

Para que ela receba a data de hoje, utilizaremos LocalDate.now(), que retorna a data atual. Isso também se aplica a LocalTime e LocalDateTime.

LocalDate dataCompra = LocalDate.now();

Determinar o ano, mês e dia usando LocalDate.of().

LocalDate dataPrimeiraParcela = LocalDate.of(2025, 5, 15);

Se quisermos que a variável dataSegundaParcela receba uma data 30 dias após a primeira parcela, utilizaremos plusDays(30).

LocalDate dataSegundaParcela = dataPrimeiraParcela.plusDays(30);

Usamos o plusDays(), mas existem outros métodos disponíveis para todos. Por exemplo:

Verificando datas

Além de adicionar e subtrair datas, podemos fazer verificações, como se uma data é antes ou depois de outra com os seguintes métodos:

Por exemplo, podemos verificar se a data da primeira parcela é igual à data atual usando isEqual(LocalDate.now()), que retorna um valor booliano. Acima dos prints, criaremos o seguinte bloco para verificar se a data atual é o dia do vencimento:

                if (dataPrimeiraParcela.isEqual(LocalDate.now())) {
                    System.out.println("Hoje é o dia do vencimento");
                } else {
                    System.out.println("Ainda não está no dia do vencimento");
                }
            

Formatando datas

O LocalDate segue o padrão ISO 8601, que exibe datas no formato ano-mês-dia. No entanto, podemos querer mostrar em um padrão diferente.

Podemos formatar a data usando DateTimeFormatter para criar o formato no padrão brasileiro. Por exemplo, abaixo dos prints, podemos criar um DateTimeFormatter chamado formato, usando DateTimeFormatter.ofPattern("dd/MM/yyyy"), onde dd/MM/yyyy define o padrão dia/mês/ano.

Utilizamos MM maiúsculo para definir meses, pois mm minúsculo define os minutos.

DateTimeFormatter formato = DateTimeFormatter.ofPattern("dd/MM/yyyy");

System.out.println("Data compra formatada: " + dataCompra.format(formato));

Trabalhando com fusos horários

Além de podermos formatar as datas no padrão desejado, escolhendo se queremos apenas o mês e o ano, se queremos o ano com quatro dígitos ou com dois, e definir exatamente o padrão através do DateTimeFormatter.ofPattern(), também podemos trabalhar por meio da orientação a fusos horários.

Vamos supor que queiramos trabalhar com uma data baseada em um fuso horário e que possamos verificar qual seria essa data em outro fuso horário. Para fazer isso, vamos criar uma data de conclusão da compra para armazenar o momento exato da conclusão da compra.

Queremos observar esse horário de conclusão tanto no Brasil quanto no padrão dos Estados Unidos. Para isso, utilizaremos a classe ZonedDateTime, que é um DateTime baseado no fuso horário.

Vamos criar uma variável dataConclusaoCompra e definir como ZonedDateTime.now(), que representa o momento atual.

ZonedDateTime dataConclusaoCompra = ZonedDateTime.now();

ZonedDateTime dataCompraNy = dataConclusaoCompra.withZoneSameInstant(ZoneId.of("America/New_York"));

Calculando a diferença entre datas e horas

Agora, faremos um exercício sobre o cálculo de diferença entre datas e horas, utilizando Duration e Period.

O Duration é geralmente usado para horas. Vamos fazer um teste para ver nosso horário de trabalho (quanto tempo de expediente estamos trabalhando).

                LocalTime inicio = LocalTime.of(9, 0);
                LocalTime fim = LocalTime.of(17, 30);
                Duration duracao = Duration.between(inicio, fim);
                System.out.println("Duração do expediente: " + duracao.toHours() + 
                   " horas e " + duracao.toMinutesPart() + " minutos.");
            

Agora para calcular dias:

                LocalDate dataPagamento = LocalDate.parse("2025-10-30");
                LocalDate dataPag2 = dataCompra.plusDays(45);
                
                long totalDias = ChronoUnit.DAYS.between(dataPag2);

                System.out.println("Data da compra: " + dataCompra + "\nData Pagamento: " + dataPag2 + "\nDiferença em dias: " + totalDias);
            

Mostrando Dia e Hora

                LocalDateTime dataAtual = LocalDateTime.now();
                DateTimeFormatter formato2 = DateTimeFormatter.ofPattern("EEEE dd MMMM yyyy H:mm", Locale.of("pt", "BR"));

                System.out.println(dataAtual.format(formato2));
            

Saída:

quarta-feira 11 fevereiro 2026 15:48

Criando atalho no IntelliJ

Agora, toda vez que você digitar sc + Tab, o código será inserido.

Contrutores Diferentes com herança

                public class Aluno {
                    protected String nome;
                    protected String tipo;

                    
                    public Aluno(String nome, String tipo) {
                        this.nome = nome;
                        this.tipo = tipo;
                    }
                    
                    public void identificar(){
                        System.out.printf("Aluno: %s, Tipo: %s.\n", nome, tipo);
                    }
                }
            
                public class Bolsista extends Aluno{
                    
                    public Bolsista(String nome) {
                        super(nome, "Bolsista");
                    }   
                    
                    

                }
            

Para saber mais: coesão versus acoplamento

Talvez você já tenha ouvido esses dois termos em alguma aula, artigo ou em algum lugar da internet: coesão e acoplamento. Eles aparecem bastante quando a conversa é sobre qualidade de código — e por um bom motivo.

Vamos pensar juntos aqui.

Imagine que você está organizando sua casa. Você tem uma gaveta só para meias, outra para documentos, outra para ferramentas. Tudo está separado por função. Isso é coesão. Cada “parte do sistema” faz uma coisa só, e faz bem.

Agora, imagine que toda vez que você precisa trocar uma lâmpada, tem que abrir a gaveta das ferramentas, que só pode ser aberta depois que a gaveta dos documentos estiver destrancada, que por sua vez precisa da chave que está na caixa de meias. Complicado, né?

Isso é acoplamento: uma dependência excessiva entre partes que deveriam ser independentes. Quando um detalhe muda em uma, todas as outras quebram junto.

No mundo da programação orientada a objetos, buscamos sempre alta coesão e baixo acoplamento. Isso significa que cada classe deve ter uma responsabilidade clara, e que essas classes devem conversar entre si de forma leve, sem amarras pesadas.

Exemplo prático

Se você tem uma classe Relatorio, ela deve saber como organizar os dados de um relatório. Só isso. Ela não deve saber como enviar e-mails, exportar para PDF ou salvar no banco de dados. Essas tarefas devem ficar com outras classes (EmailService, ExportadorPDF, Repositorio), e todas elas devem se comunicar de forma simples — idealmente por meio de interfaces ou contratos bem definidos.

Código ruim: com baixa coesão e alto acoplamento:

                public class Relatorio {
                    private String titulo;
                    private String conteudo;
                
                    public Relatorio(String titulo, String conteudo) {
                        this.titulo = titulo;
                        this.conteudo = conteudo;
                    }
                
                    public void gerarRelatorio() {
                        System.out.println("Relatório: " + titulo);
                        System.out.println("Conteúdo: " + conteudo);
                        enviarPorEmail(); // Mistura responsabilidades!
                        salvarNoBanco();  // Aumenta acoplamento!
                    }
                
                    private void enviarPorEmail() {
                        System.out.println("Enviando por e-mail...");
                    }
                
                    private void salvarNoBanco() {
                        System.out.println("Salvando no banco de dados...");
                    }
                }
            

Aqui, a classe Relatorio faz tudo: exibe, envia e salva. Ela não é coesa (muitas responsabilidades) e está fortemente acoplada a outras ações (e-mail e banco).

Código bom: alta coesão e baixo acoplamento:

                public class Relatorio {
                    private String titulo;
                    private String conteudo;
                
                    public Relatorio(String titulo, String conteudo) {
                        this.titulo = titulo;
                        this.conteudo = conteudo;
                    }
                
                    public String getTextoFormatado() {
                        return "Relatório: " + titulo + "\nConteúdo: " + conteudo;
                    }
                }
                
                public class EmailService {
                    public void enviar(String mensagem) {
                        System.out.println("Enviando e-mail...");
                        System.out.println(mensagem);
                    }
                }
                
                public class BancoDeDados {
                    public void salvar(String dados) {
                        System.out.println("Salvando no banco...");
                        System.out.println(dados);
                    }
                }
            

E no main:

                Relatorio rel = new Relatorio("Status 2025", "Tudo estável.");
                EmailService email = new EmailService();
                BancoDeDados bd = new BancoDeDados();
                
                String texto = rel.getTextoFormatado();
                email.enviar(texto);
                bd.salvar(texto);
            

Aqui, cada classe tem uma única responsabilidade, e as dependências são explícitas, controladas e independentes. Mudanças em uma não quebram as outras.

Por que isso importa?

Porque sistemas mudam. E quanto mais coeso e menos acoplado for seu código, mais fácil será mantê-lo, testá-lo, refatorá-lo e expandi-lo. Você consegue trocar uma peça sem quebrar todo o mecanismo. Se tem algo que diferencia um sistema que só "funciona" de um sistema que evolui bem com o tempo, é essa dupla: coesão alta, acoplamento baixo.

Pense nisso ao criar sua próxima classe: Essa classe está realmente focada no que ela deveria fazer? Ela depende de mais coisas do que deveria?

Essas perguntas não têm respostas fixas, mas quanto mais você se fizer elas durante o desenvolvimento, melhor será o design das suas soluções.

String e Regex

trim()

Utilizamos o método trim() para remover os espaços em branco no início e no final da string.

String nome = " João da Silva ";

System.out.println(nome.trim());

Saída: João da Silva


Convertendo para Maiúcula e para Minúscula

String texto = "Olá, Mundo!";

Utilizamos toLowerCase() para converter uma String para Minúsculo.

System.out.println(texto.toLowerCase());

Agora para passarmos para Maiúsculo utilizamos toUpperCase().

System.out.println(texto.toUpperCase());


Substituindo parte de uma String

Para substituirmos uma parte de uma String utilizamos replace(Aqui coloca a parte que será substituída,aqui a nova String);

Agora para checarmos que uma String contem determinada String utilizamos contains("String que deve conter na String")

                    System.out.println("Digite o texto: O gato caça o rato.");
                    String texto = sc.nextLine();
            
                    if(texto.contains("gato")){
                        System.out.println("Informe outro animal para caçar o rato: ");
                        String animal = sc.nextLine();
                        System.out.println(texto.replace("gato" ,animal ));
                        break;
                    }
                    System.out.println("O texto não tem gato!");
                

Extraindo parte de uma String

                        String string = "relatorio_final.pdf";
                        //substring(início - o caractere dessa posição é adicionada na string final,
                        //fim - o caractere dessa posição é excluída da string final)
                        //lastIndexOf(retorna o índice da última ocorrência encontrada)
                        System.out.println(string.substring(0,string.lastIndexOf(".")));
                    

Verificando se uma palavra contem em uma string

                        Scanner sc = new Scanner(System.in);
                        System.out.println("Digite um texto: ");
                        String string = sc.nextLine();
                
                        System.out.println("Digite uma palavra que o texto contenha: ");
                        String encontrar = sc.nextLine();
                        if (string.contains(encontrar)) {
                            System.out.printf("A palavra \"%s\" está presente no texto.\n", encontrar);
                        }else {
                            System.out.printf("A palavra digitada '%s' não foi encontrada\n", encontrar);
                        }
                        sc.close();        
                    

Formatando números

                        String numero = "19,0532";
                        System.out.println("R$ " + numero.substring(0, numero.indexOf(",") + 3));
                        System.out.printf("R$ %.2f", Double.parseDouble(numero.replace(",",".")));
                        String valorFormatado = String.format("R$ %.2f", Double.parseDouble(numero.replace(",",".")));
                        System.out.println("\nValor formatado: " + valorFormatado);
                    

Validando formato

                        Scanner sc = new Scanner(System.in);
                        while (true){
                            System.out.println("Digite um padrão no formato ABC-1234: ");
                            String string = sc.nextLine();
                
                            Pattern pattern = Pattern.compile("^[a-zA-Z]{3}-\\d{4}$");
                            Matcher matcher = pattern.matcher(string );
                
                            if (matcher.matches()) {
                                System.out.printf("Código digitado '%s' corresponde ao formato.", string);
                                break;
                            }else{
                                System.out.printf("Código digitado '%s' não corresponde ao formato.", string);
                            }
                        }
                
                        sc.close();
                    

Validando CPF com Pattern

                            Scanner sc = new Scanner(System.in);
                            
                            String string;
                            Matcher matcher;
                    
                            Pattern pattern = Pattern.compile("^\\d{3}.\\d{3}.\\d{3}-\\d{2}$");
                    
                            while (true){
                                System.out.println("Digite o CPF no formato 123.456.789-09: ");
                                string = sc.nextLine();
                    
                                matcher= pattern.matcher(string);
                    
                                if(matcher.matches()){
                                    System.out.printf("CPF %s no formato correto!\n", string);
                                    break;
                                }else{
                                    System.out.printf("CPF %s não esta no formato correto!\n", string);
                                }
                            }
                            sc.close();
                        

Validando cpf com String

                            Scanner scanner = new Scanner(System.in);
                            
                            System.out.print("Digite o CPF: ");
                            String cpf = scanner.nextLine();
                    
                            String regex = "\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2}";
                            if (cpf.matches(regex)) {
                                System.out.println("O CPF " + cpf + " está no formato válido.");
                            } else {
                                System.out.println("O CPF " + cpf + " não está no formato válido.");
                            }
                            scanner.close();
                        

Extraindo Hashtags

                            String string = "#mundo! Estou aprendendo #Java e #programação";
                            
                            Pattern pattern = Pattern.compile("#\\w+");
                            Matcher matcher = pattern.matcher(string);
                    
                            ArrayList<String> hashtags = new ArrayList<>();
                    
                            while (matcher.find()){
                                hashtags.add(matcher.group());
                            }
                            if (hashtags.isEmpty()) {
                                System.out.println("Nenhuma hashtag encontrada.");
                            } else {
                                System.out.println("Hashtags encontradas: " + String.join(", ", hashtags));
                            }
                        

saída:

Hashtags encontradas: #mundo, #Java, #programa


Validando senha

                            Scanner sc = new Scanner(System.in);
                            String senha;
                    
                            String regex = "(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
                            while (true){
                                System.out.println("Senha: ");
                                senha = sc.nextLine();
                    
                                if (senha.matches(regex)) {
                                    System.out.println("A senha é válida.");
                                    break;
                                } else {
                                    System.out.println("A senha não é válida.");
                                }
                                System.out.println("digite (0) para sair ou (1) para continuar: ");
                                senha = sc.nextLine();
                                if(senha.equals("0")) break;
                            }
                            sc.close();
                        
  • Os Delimitadores
  • ^ : Indica o início da linha. A validação deve começar do primeiro caractere.
  • $ : Indica o fim da linha. A senha inteira deve seguir o padrão, sem caracteres extras no final.
  • Os "Lookaheads" (As Regras de Validação)
  • As partes entre (?= ... ) são chamadas de positive lookaheads. Elas verificam se algo existe na string sem "consumir" os caracteres (elas olham para frente e voltam para o início).
  • (?=.*[a-z]) : Garante que haja pelo menos uma letra minúscula.
  • (?=.*[A-Z]) : Garante que ja pelo menos uma letra maiúscula
  • (?=.*\\d) : Garante que haja pelo menos um dígito (número).
  • (?=.*[@$!%*?&]) : Garante que haja pelo menos um caractere especial da lista permitida.
  • O Conteúdo e Comprimento
  • [a-za-z\\d@$!%*?&]{8,} : Define quais caracteres são permitidos e o tamanho da senha.
    • [a-za-z\\d@$!%*?&] : Permite letras (maiúsculas e minúsculas), números e os símbolos listados.
    • {8,} : Define que a senha deve ter no mínimo 8 caracteres.

Resumo da Regra

    Para a senha ser considerada válida (true), ela precisa:
  • Ter pelo menos 8 caracteres.
  • Conter ao menos uma letra minúscula.
  • Conter ao menos uma letra minúscula
  • Contar ao menos uma letra maiúscula
  • Conter ao menos um número.
  • Conter ao menos um caractere especial (como @, $, !, etc.).
1
Teste

Coleções e Streams

Para saber mais: entendendo Maven e Gradle

Comparação entre Maven e Gradle

Para entender a diferença entre Maven e Gradle, precisamos primeiro compreender o que eles são e para que servem. Maven e Gradle são ferramentas de automação de compilação e gerenciamento de dependências muito populares na comunidade Java. Eles ajudam a simplificar e organizar o processo de construção, teste e implantação de projetos Java, tornando o desenvolvimento mais eficiente.

O que é Maven?

Principais conceitos do Maven:

O que é Gradle?

O Gradle é outra ferramenta de construção e automação de projetos Java que ganhou popularidade ao longo dos anos. Ele usa uma linguagem de domínio específico (DSL) baseada em Groovy ou Kotlin para definir a estrutura do projeto e as tarefas de construção.

Principais conceitos do Gradle:

Agora que temos uma compreensão básica do que são Maven e Gradle, vamos ver suas semelhanças, diferenças, vantagens e desvantagens.

Semelhanças

Tanto o Maven quanto o Gradle fornecem convenções para a estrutura do diretório do projeto, dependência de gerenciamento e plugins de construção. Eles também são amplamente suportados em IDEs e ferramentas de Integração Contínua (CI).

Diferenças

A principal diferença entre Maven e Gradle é a maneira como eles gerenciam as dependências e como eles descrevem a lógica de construção. Maven usa arquivos XML para gerenciar as dependências e descreve a lógica de construção usando plugins, enquanto Gradle usa um formato de script e descreve a lógica de construção como código.

Vantagens e Desvantagens

Maven é fácil de aprender e tem um grande ecossistema. A desvantagem é que os arquivos XML podem se tornar muito grandes e difíceis de gerenciar para projetos complexos.

Gradle, por outro lado, permite scripts de construção mais poderosos e é mais flexível. No entanto, é mais difícil de aprender e o ecossistema ainda não é tão grande quanto o do Maven.

A escolha entre Maven e Gradle depende do seu projeto e preferências. Ambas as ferramentas são poderosas e amplamente utilizadas, e ambas têm vantagens e desvantagens. O Maven é mais rígido e segue uma abordagem "opinião sobre configuração", o que pode ser uma vantagem para projetos menores e mais simples. Já o Gradle é mais flexível e personalizável, sendo uma escolha popular para projetos maiores e complexos que requerem configurações específicas.

Em resumo, tanto Maven quanto Gradle podem ser usados com eficiência para projetos Java, e a escolha dependerá do contexto e das preferências da equipe de desenvolvimento.

Para saber mais: a interface CommandLineRunner

A interface CommandLineRunner é um recurso poderoso dentro do universo do Spring Framework, amplamente utilizado no desenvolvimento de aplicações Java. Ela permite que executemos alguma ação logo após a inicialização de nossa aplicação. Pode ser muito útil, por exemplo, se quisermos carregar alguns dados em nosso banco de dados logo na inicialização de nossa aplicação.

Como funciona?

Quando uma aplicação Spring Boot é lançada pode ocorrer várias operações automáticas, como a criação de beans, configuração de banco de dados, entre outros. A abertura para customização destas operações é limitada, e é aqui que a interface CommandLineRunner entra em cena.

A interface CommandLineRunner representa uma tarefa a ser executada após a inicialização do Spring Boot, ou seja, permite definir código para ser executado automaticamente quando o aplicativo é iniciado.

Como Usar?

A utilização é bem simples. Na própria classe principal da aplicação, podemos incluir para que se implemente a interface CommandLineRunner. Veja um exemplo:


                @SpringBootApplication
                public class MyCommandLineRunner implements CommandLineRunner {
                   
                   @Override
                    public void run(String... args) throws Exception {
                        System.out.println("Olá, Mundo!");
                    }
                }

            

Note que no exemplo acima, criamos uma classe chamada MyCommandLineRunner que implementa a interface CommandLineRunner. No método "run" inserimos a ação que desejamos que seja executada logo depois que a aplicação for iniciada, nesse caso, apenas printamos "Olá, Mundo!".

Quando Usar?

A interface CommandLineRunner é bem versátil e pode ser usada em diversas situações. Conforme mencionado anteriormente, ela pode ser usada para carregar dados para um banco de dados. Também pode ser usada para iniciar recursos, como conexões de rede, e para checar a integridade de determinados componentes ou serviços com os quais a aplicação irá interagir.

É importante lembrar que a CommandLineRunner é executada apenas na inicialização da aplicação, então não deve ser utilizada para tarefas que precisam ocorrer periodicamente durante o funcionamento da aplicação, para isso, Spring oferece outras ferramentas que serão mais adequadas.

Motivação

Vamos imaginar uma situação onde temos que carregar uma grande quantidade de dados em nosso banco de dados assim que nossa aplicação Spring iniciar. Bem, manualmente isso seria desafiador e demorado, no entanto, a interface CommandLineRunner torna essa tarefa extremamente mais simples.

Ao aprofundar seu conhecimento no Spring, você terá diversas opções para otimizar suas aplicações e tornar seu código mais limpo e eficaz. O Spring é um framework que facilita o desenvolvimento de aplicações em Java. Ele oferece um modelo de programação abrangente e simplificado, escondendo muitos dos detalhes de baixo nível. Como resultado, você pode se concentrar em escrever o seu código, sem se preocupar com uma infinidade de detalhes técnicos.