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