Pular para o conteúdo principal

Guia da certificação Java SE 8 Programmer l - Parte 5: Métodos e encapsulamento

Seja bem-vindo a série de postagens sobre a certificação Java. Como funciona, o que fazer para comprar, marcar o dia da prova e o principal, o que estudar.

Para ver o índice da série e as datas das publicações, acesse este link

Parte 5 – Métodos e encapsulamento

Neste post, vamos nos aprofundar um pouco mais na orientação a objetos e ver como funcionam os métodos, seus modificadores, sobrecarga e um pouco de lambdas.

Objetivos do exame

  • Criando métodos que retornam valores
  • Sobrecarga de métodos
  • Métodos estáticos
  • Modificadores de acesso
  • Encapsulamento
  • Passagem de valor e passagem de parâmetro
  • Escrevendo expressões lambda simples

Criando métodos

Um método pode conter até sete elementos, sendo que alguns obrigatórios e outros opcionais. No exemplo abaixo o método conhecido de todo programador Java:

public static void main(String[] args) {
   System.out.println("hello!!");
}

O método acima contém seis elementos, onde o sétimo seria uma lista de exceções.

public static void main(String[] args) throws Exception {
   System.out.println("hello!!");
}

Agora nosso método main pode propagar uma exceção, porém sem necessidade neste caso. Exceptions serão explicadas posteriormente. Voltando para o foco dos elementos do método, temos o seguinte:

Tipo do Elemento
Nome do Elemento
Obrigatório
Modificador
public
Não
Modificador opcional
static
Não
Tipo de retorno
void
Sim
Nome do método
main
Sim
Lista de parâmetros
(String[] args)
Sim. Aceita ( )
Lista de exceções
throws Exception
Não
Corpo
{ System.out.println("hello!!");}
Sim. Aceita { }
Esta é a definição do método main do Java, porém quando escrevemos nossos próprios métodos, teremos algo como:

public void printName(){
   System.out.println(this.name);
}

Para chamar o método, apenas utilizamos printName();

Modificadores de acesso

Agora, precisamos entender como funcionam os modificadores de acesso. Começando com os tipos de modificadores:

public: O método pode ser chamado por qualquer outra classe.

private: 
O método pode ser acessado somente dentro da própria classe.

protected:
O método só pode ser chamado de classes no mesmo pacote ou subclasses.

“Default”: 
O método só pode ser chamado de classes no mesmo pacote. Não é necessário o colocar o modificador no método.
No exame é possível que a ordem dos elementos esteja incorreta ou sejam utilizados palavras reservadas não compatíveis com a assinatura do método, Ex:

default void printName(){ // não compila
   System.out.println(this.name);
}

default é uma palavra reservada para o uso em switch case ou em interfaces.
void public printName(){ // não compila
   System.out.println(this.name);
}

Não compila pois o identificador está invertido e o Java não reconhece.
void printName(){}

O método acima é válido apesar de não fazer nada.

Modificadores opcionais

Além dos modificadores de acesso, podemos utilizar os modificadores opcionais. Eles também farão com que o seu método mude a forma de acesso e podem ser combinados. Abaixo a lista de modificadores que aparecem na prova.


static: Métodos de classe. Podem ser invocados sem necessidade de criar uma instância.

abstract: 
Utilizado quando o método não provém uma implementação.


final: 
Utilizado com herança para não permitir a sobrescrita do método.
Dica: verifique sempre se o tipo de retorno está exatamente antes do nome do método.

Tipos de retorno

O retorno de um método pode ser qualquer tipo de objeto ou primitivo. Caso o método não possua um retorno, devemos utilizar void. Caso for definido algum tipo de retorno diferente de void, então no corpo do método deverá ter obrigatoriamente um return.

Obs: métodos void também podem ter o return.

void printName(){
   return// OK
}
String getName(){
   return// não compila
}
String getName(){
   return ""// OK
}

return sempre deve ser seguido do tipo esperado na declaração do método.

Nome do método

Os nomes dos métodos seguem as mesmas regras que praticamos com nomes de variáveis. Um identificador só pode conter letras, números, $ ou _.

Lista de parâmetros

Embora a lista de parâmetros seja obrigatória, ela não precisa conter nenhum parâmetro. Basta abrir e fechar os parênteses. Para uma lista de parâmetros, basta separá-los por vírgula.

Lista de exceções

Por enquanto, basta saber que um método pode lançar nenhuma, uma ou mais exceções. Ex:

void printName(){}
void 
printName() throws Exception{}
void 
printName() throws RuntimeException, Exception{}

Os três métodos acima são válidos. Em outros posts estudaremos mais a fundo as exceções.

Corpo do método

O corpo do método é simplesmente um bloco de código Java onde as coisas acontecem. O corpo está entre as chaves que são obrigatórias na declaração do método.

Varargs

Aqui você deve lembrar que o varargs deve ser o único e último parâmetro do método.

void printNames(String... names) {
   System.out.println(names);
}
void printNames(String... names, String address) { // Não compila
void printNames(String... names, String... address) { // Não compila
void printNames(String address, String... names) { // OK

Para chamar um método que contém varargs, temos duas opções. Passando como um array ou uma lista de parâmetros separados por vírgula.
printNames("Joao, Maria, Isabel");

Ou:

printNames(new String[]{"Joao, Maria, Isabel"});
void printNames(String... names) {

Também é válido passar um array sem elementos para o método printNames()

Trabalhando com os modificadores de acesso

Do modo mais restritivo (private) ao modo mais acessível (public) temos o default e o protected que podem nos confundir um pouco. Em ordem de restrição temos: private, default, protected e public. Os modificadores de acesso são aplicados para métodos e atributos da classe.

Acesso default

Um método default (sem modificador) significa que o método será acessível apenas dentro do próprio pacote. Também conhecido como (Package Private Access). Dada a classe Animal abaixo:

package com.giacom.oca1.animal;
public class Animal {
   private int age;
   private String name;
   public Animal(int age, String name) {
       this.age = age;
       this.name = name;
   }
   int getAge() {
       return age;
   }
}

Podemos invocar o método getAge() de outra classe no mesmo pacote:
package com.giacom.oca1.animal;
public class Fish {
   public static void main(String[] args) {
       Animal animal = new Animal(12"cat");
       System.out.println(animal.getAge());
   }
}

Caso mude a classe Fish de pacote, vamos precisar importar a classe Animal e o método getAge() não estará mais visível.
package com.giacom.oca1.animal.fish;
import com.giacom.oca1.animal.Animal;
public class Fish {
   public static void main(String[] args) {
       Animal animal = new Animal(12"cat");
       System.out.println(animal.getAge()); // Erro de compilação
   }
}

Acesso protected

Os atributos e métodos protected permitem ser acessados também pelas suas subclasses, além das classes do mesmo pacote. Alterando a nossa classe Animal deixando o método getAge() como protected.
protected int getAge() {
   return age;
}

Agora, além de dar acesso a classes do mesmo pacote, também estará acessível pelas classes que estendem de Animal independente do pacote que estejam.
public class Fish extends Animal {
   public static void main(String[] args) {
      Animal animal = new Animal(2"fish");
      System.
out.println(animal.getAge());
   }
}

Acesso público

Como o nome já indica, os membros declarados como public serão acessíveis por qualquer classe de qualquer pacote. Lembrando que se estiver em um pacote diferente, a classe deve ser importada.

public class 
Animal {
   int age;
   public String name;
   public Animal(int age) {
       this.age = age;
   }
   public int getAge() {
       return age;
   }
}

Tornando nossa classe Animal com acessos públicos. Agora qualquer classe em qualquer pacote pode utilizar os membros públicos da classe Animal.
Animal animal = new Animal(2);
animal.name "fish";
System.out.println(animal.getAge());

Atributos e métodos estáticos

Membros estáticos não necessitam que um objeto seja instanciado. São considerados membros de classe e não de instância. Sendo assim, todas as instâncias e classes compartilham do mesmo membro independente de quantas instâncias existirem.

class Counter{
   public static int count;
   public void showCount(){
       System.out.println(count);
   }
}
A classe acima possui o atributo count com o modificador de acesso public static.
public static void main(String[] args) {
   Counter counter1 = new Counter();
   counter1.count++;
   counter1.showCount();
   Counter counter2 = new Counter();
   counter2.count++;
   counter2.showCount();
   counter1.showCount();
}

Você saberia responder o que será impresso ao ser executado o código acima? Veja que temos duas instâncias de Conter e cada uma adiciona um valor a count. Na última linha temos a chamada do método showCount() de counter1 que incrementou a variável apenas uma vez. Naturalmente o valor de count seria 1 se não fosse o modificador static. Com isso, todas as vezes que o count for modificado, todas as instâncias passam a ter o mesmo valor. Portanto a saída do código acima é: 1, 2, 2.
Membros estáticos podem ser invocados através da variável de instância ou pelo próprio nome da classe. Alterando nosso código, poderíamos ter: Counter.count++;
Preste atenção em um detalhe do Java. Como você pode invocar um membro estático através de uma variável de instância, o Java entende o membro estático e realiza a chamada como se fosse utilizar o próprio nome da classe. Ficou confuso? Vamos alterar nosso código:

Counter counter1 = null;
counter1.count++;
Counter counter2 = new Counter();
counter2.count++;
counter2.showCount();

Veja que counter1 é null e mesmo assim podemos incrementar a variável count. Por mais estranho que pareça, o código acima compila e a saída é 2.

Estático versus Instância

Um membro estático somente pode acessar outros membros estáticos. Caso tente acessar uma variável ou método de instância, seu código não irá compilar. Ex:

String name;
public static void main(String[] args) {
   System.out.println(name);// Erro de compilação
}

Mensagem do compilador: Error:(8, 28) java: non-static variable name cannot be referenced from a static context.
Obs: No exame é comum uma pergunta do tipo: O que será preciso alterar para que o código compile? Neste caso, podemos incluir o modificador static na variável name.

Modificador final

Muito comum na utilização de constantes, indica que uma vez inicializada, a variável não pode ser alterada. Qualquer tentativa de alteração irá resultar em um erro de compilação.

private static final String NAME "Name";
public static void main(String[] args) {
   NAME "New Name"// Erro de compilação
}

Inicializando estáticos

Podemos inicializar variáveis estáticas de duas formas. No momento da declaração ou em chamados blocos estáticos.


private static String name;
static {
   name "Name";
}
public static void main(String[] args) {
   System.out.println(name);
}
Blocos de inicialização podem ser colocados em qualquer parte da classe (inclusive dentro dos métodos), e isso pode nos confundir no momento da prova. O que o código abaixo imprime?

private static String name = "First";
static {
   name "Name";
}
public static void main(String[] args) {
   System.out.println(name);
}
static {
   name "New Name";
}

Neste caso, o último bloco static será considerado e a saída será “New Name”. E agora, qual será a saída?

private static String name "First";
{
   name "Name";
}
public static void main(String[] args) {
   System.out.println(name);
}
{
   name "New Name";
}

A saída agora é "First" porque removemos os blocos de inicialização estáticos. Os blocos de inicialização agora são de instância.
Quando incluirmos o modificador final em uma variável estática, teremos que prestar mais atenção. Devemos verificar algumas regras básicas:
  1. Variáveis static final devem ser inicializadas.
  2. Não podemos ter mais de uma inicialização para a variável
Vejamos alguns exemplos:
1) Não inicializada:
private static final String name//Erro de compilação

2) Inicialização em um bloco:
private static final String name;
static {
   name "ssss";
}

3) Tentativa de inicializar mais de uma vez:
private static final String name;
static {
   name "Name";
  
name "Last"// Erro de compilação
}

Imports estáticos

Antes da existência dos imports estáticos, para utilizar um membro estático era necessário colocar o nome da classe ou variável de instância. Agora é possível reduzirmos um bocado de código redundante utilizando os imports estáticos.

double num = Math.random();
double exp = Math.getExponent(9.0);
double abs = Math.abs(100.05);
Utilizando os imports estáticos, temos:
double num = random();
double exp = getExponent(9.0);
double abs = abs(100.05);

Nosso bloco de imports, teremos:
import static java.lang.Math.abs;
import static java.lang.Math.getExponent;
import static java.lang.Math.random;
É possível também utilizar o curinga:
import static java.lang.Math.*;

Lembre-se de que o que deve ser importado é o membro estático e não a classe.

import static java.lang.Math; // Não compila
A palavra chave import deve ser a primeira da linha.

static import java.lang.Math.random; // Não compila
Existe um import estático, porém o nome da classe é utilizado sem um import.

import static java.util.Arrays.*;
List<String> names = Arrays.asList("Lisa""Amanda"); // Erro de compilação

Faltando o import de java.util.Arrays; (sem static)

Passagem por valor ou referência?

O Java suporta apenas passagem por valor, ou seja, ao receber um parâmetro no método, é feita uma cópia da variável do ponto que chamou o método.
public static void main(String[] args) {
   String name = "Name"; 
   changeName(name);
   System.out.println(name); // print "Name"
}
static void changeName(String name) {
   name = "Last";
}

No código acima, por mais que a variável name tenha sido alterada no método changeName, ela permaneceu com o mesmo valor. O que foi alterada foi a sua cópia. Por coincidência o nome do parâmetro tem o mesmo nome. Mas isto não interfere em nada.
Agora observe o código abaixo:

public static void main(String[] args) {
   StringBuilder sb = new StringBuilder();
   append(sb);
   sb.append(" Name");
   System.out.println(sb); // print "Last Name"
}
static void append(StringBuilder sb) {
   sb.append("Last");
}

Porque este código imprime “Last Name”? Porque uma cópia da referência de StringBuilder é passada para o método. A variável original e a cópia apontam para o mesmo objeto, então a alteração é refletida em todas as variáveis que apontam para aquele objeto.

Sobrecarga de método

Podemos ter vários métodos com o mesmo nome na mesma classe, porém devemos ter uma lista de parâmetros diferentes. Isso se chama sobrecarga de método. Um exemplo clássico de sobrecarga de método é a classe StringBuilder:


Temos vários métodos append com tipos de parâmetros diferentes. Para o exame é preciso reconhecer se uma sobrecarga de método é válida ou não.
void showName(String name){
   System.out.println(name);
}
String showName(String name){
   return name;
}

No exemplo acima, temos um problema. Por mais que o tipo de retorno seja diferente, temos o mesmo nome de método com os mesmos tipos de parâmetros. Neste caso a classe não irá compilar. Outra questão do exame é referente a qual método será chamado dependendo do valor passado na chamada.

public static void main(String[] args) {
   showNumber(1);
}
static void showNumber(int num) {
   System.out.println("int");
}
static void showNumber(short num) {
   System.out.println("short");
}
static void showNumber(byte num) {
   System.out.println("byte");
}

Qual será a saída da execução do bloco de código acima? "int" pois estamos passando um literal “1” que é convertido automaticamente para int. Para que seja chamado o método correto conforme o tipo, é preciso deixar explícito o tipo. Passando uma variável do mesmo tipo ou fazendo a conversão no momento da chamada:

public static void main(String[] args) {
   showNumber((byte)1); //print byte
  showNumber((short)1); //print short
}

Ou:

public static void main(String[] args) {
   byte b = 1; 
  showNumber(b); //print byte
}

Sobrecarga com Varargs

Um parâmetro Varargs é convertido automaticamente para um Array, então cuidado quando se deparar com métodos como o abaixo:


static void showNumber(int... num) {
   System.out.println("int");
}
static void showNumber(int[] num) {
   System.out.println("int");
}

O código acima não compila.

O que ocorre quando temos um método com parâmetro primitivo e a versão Object daquele tipo primitivo?

public static void main(String[] args) {
   showNumber(1);
}
static void showNumber(int num) {
   System.out.println("int");
}
static void showNumber(Integer num) {
   System.out.println("Integer");
}

O código acima compila sem problemas e a saída é "int" pois o Java encontra o mesmo tipo e não precisará fazer a conversão para o tipo Objeto. Caso o método com o parâmetro primitivo não estivesse presente, o método com o parâmetro Integer seria chamado.

Construtores

Construtores são métodos especiais que tem o mesmo nome da classe e não possuem tipo de retorno. Abaixo um exemplo de construtor sem parâmetros:

public class Person {
   public Person() {
       System.out.println("Constructor...");
   }
}

O Java é case sensitive, então preste atenção se ver um método sem o tipo de retorno e com o mesmo nome da classe, porém com letras minúsculas ou qualquer coisa diferente do nome da classe. Se trata de um erro de compilação:

public person() {
   System.out.println("invalid constructor");
}

O código acima não é um método válido pois não tem tipo de retorno e também não é um construtor pois não é igual ao nome da classe.
Caso você não saiba, o Java gera um construtor padrão quando o mesmo não for definido. Porém se você definir construtores com parâmetros, o construtor padrão não será gerado.

public class Person {
   String name;
   String age;
    public Person(String name, String age) {
       this.name = name;
       this.age = age;
   }  
   public static void main(String[] args) {
       Person p = new Person(); //Erro de compilação   
   }
}

O código acima não compila, pois não temos um construtor sem parâmetros, apenas o construtor recebendo o name e o age.
Utilizamos construtores com parâmetros para inicializar nossos atributos da instância. Geralmente utilizamos a palavra chave this para nos referir ao atributo. Porém no exame é possível ver algo do tipo:

public Person(String name, String age) {
   name = name;
   this.age = age;
}

Veja que a variável de instância “name” nunca será atribuída, pois o que está sendo manipulado é a variável local do método. Para resolver isso, pode-se alterar o nome do parâmetro ou utilizar o this.

public Person(String personName, int age) {
   name = personName;
   this.age = age;
}

Agora a variável name será inicializada corretamente. Assim como os métodos, os construtores podem ser sobrecarregados e as regras para a sobrecarga são as mesmas.
Construtores podem chamar outros construtores? Sim, porém existem algumas regras. Não se pode utilizar a chamada a outro construtor como se fosse um método:

public Person(String name, int age) {
   this.name = name;
   this.age = age;
}
public Person(int age) {
   Person("name", age); // Erro de compilação
}

Um construtor somente pode ser utilizado na criação do objeto, ou seja, quando tivermos a palavra chave new:

new Person("name", age);

Por mais que o código acima compile dentro do construtor, ele não servirá para nada, pois não está sendo atribuído a nenhuma variável. O correto é utilizar a palavra reservada this:

public Person(int age) {
   this("name", age);
}

Obs: A chamada a outro construtor deve ser a primeira instrução do construtor, caso contrário, teremos um erro de compilação.

public Person(int age) {
   System.out.println(age);
   this("Name", age); // Não compila
}

Construtores privados

Utilizamos construtores privados quando queremos que a nossa classe não seja instanciada externamente. Isso para ter controle sobre as instâncias ou porque a nossa classe tem somente métodos estáticos:

public class Person {
   private Person() {
   }
}

class Schedule{
   public static void main(String[] args) {
       Person p = new Person(); // Erro de compilação   
   }
}

Não é possível instanciar a classe a partir de outra. Apenas a própria classe poderá criar uma instância dela mesma.

Atributos final

Outra forma de inicializamos os atributos final, é através do construtor. Porém se o Java perceber que a variável pode não ser inicializada, teremos um erro de compilação. Como assim?

final String name// Erro de compilação
int age;
public Person(String name, int age) {
   this.name = name;
   this.age = age;
}
public Person(int age) {
}

Observe que método Person(int age) não tem nada no corpo. Então se a classe for instanciada através deste construtor, a variável name ficará sem ser inicializada e isso não é permitido. Se deletarmos o método, a classe compila normalmente.

Ordem de inicialização

Devemos conhecer bem como será a ordem de inicialização de um objeto e ter isso bem claro no momento do exame. A ordem das chamadas é a seguinte:
  • Blocos estáticos * da superclasse
  • Blocos estáticos * da classe
  • Blocos não estáticos * da superclasse
  • Construtor da superclasse
  • Blocos não estáticos * da classe
  • Construtor da classe

Encapsulamento

Os métodos operam no estado interno de um objeto e servem como o mecanismo principal para a comunicação objeto-objeto. Esconder o estado interno e exigir que toda interação seja realizada através dos métodos de um objeto é conhecido como encapsulamento de dados - um princípio fundamental da programação orientada a objetos.

Com este conceito em mente, definimos nossos atributos com o acesso mais restrito possível (private) e então utilizamos os métodos para atribuir os valores e fazer as verificações necessária a fim de manter nosso objeto em um estado consistente.

private String name;
public void setName(String name) {
   if (isValidName(name)) {
       this.name = name;
   }
}
Somente será possível alterar a variável name de fora da classe se você disponibilizar um método acessível e a sua regra de negócio permitir.

Lambdas

Java 8 adicionou o conceito de programação funcional através dos lambdas. Podemos pensar em uma expressão lambda como um método anônimo. Lambdas permitem que você escreva códigos poderosos em Java. Apenas as expressões lambda mais simples estão no exame e serão descritas nesta sessão. Para saber mais sobre lambdas:

Exemplos

Busca de pessoas idosas.

Neste exemplo, vamos criar a classe Person e uma interface para verificar se a pessoa é idosa:

public class Person {
   private String name;
   private int age;
   public Person(String name, int age) {
       this.name = name;
       this.age = age;
   }
}
Classe Person. Métodos getters e setters omitidos.

public interface CheckPerson {
   boolean isOld(Person p);
}

Interface CheckPerson com o método isOld.
public class PersonSearch {

   public static void main(String[] args) {
       List<Person> people = new ArrayList<>();
       people.add(new Person("Jose"33));
       people.add(new Person("Ane"90));
       people.add(new Person("Faria"65));
       printOld(people, new CheckPersonImpl());
   }

 static void printOld(List<Person> personList, CheckPerson cp) {
       for (Person p : personList) {
           if (cp.isOld(p)) {
               System.out.println(p);
           }
       }
   }
}

Nossa classe PersonSearch que imprime somente pessoas idosas conforme a regra implementada na classe CheckPersonImpl.

public class CheckPersonImpl implements CheckPerson {

   @Override
   public boolean isOld(Person p) {
       return p.getAge() >= 80;
   }
}

Implementação da interface CheckPerson
Com o uso de lambdas, podemos substituir a implementação da interface por uma expressão lambda.

Antes: printOld(people, new CheckPersonImpl());
Depois: printOld(people, cp -> cp.getAge() >= 80);
Com o uso da expressão, eliminamos a necessidade de implementar a interface. Pense no seguinte: O segundo parâmetro que recebe uma interface é simplesmente a implementação de um método em uma classe anônima, tal como:

printOld(people, new CheckPerson() {
   @Override
   public boolean isOld(Person p) {
       return p.getAge() >= 80;
   }
});

Obs: Quando uma interface tem apenas um método, podemos substituir por uma expressão lambda.

Examinando a expressão

Nosso exemplo utilizando a expressão lambda:
 cp -> cp.getAge() >= 80

A parte da direita é a declaração da variável (cp) e a parte da esquerda após o sinal -> é a implementação do método que retorna um boolean. Várias partes do lambda são opcionais. Poderíamos ter o mesmo código desta forma:

printOld
(people, (Person cp) -> {
   return cp.getAge() >= 80;
});

Agora fica mais fácil de entender. Remete quase a implementação da interface, certo?

Partes da expressão

1 - Especifique um único parâmetro com o nome cp
2 - O operador de seta -> para separar o parâmetro e o corpo
3 - Um corpo que chama um único método e retorna o resultado desse método
Caso a expressão tenha mais de um parâmetro, é preciso colocá los entre parênteses. Alteramos nossa interface:

boolean isOld(Person p, String address);

Agora a expressão lambda deve ser:

printOld(people, (person, address) -> person.getAge() >= 80);

Veja que por mais que não utilizamos a variável address, ela deve ser informada na expressão. Caso o método não possua parâmetros, devemos também utilizar os parênteses:

boolean 
isOld();
printOld(people, () -> true);
Obs: Não se pode declarar novamente uma variável dentro do corpo da expressão lambda. Ex:

printOld
(people, p -> {
   Person p; // Erro de compilação
   return p.getAge() >= 80;
});
Obs: Se você abrir um bloco de código utilizando as chaves e o método tiver um retorno, é obrigatório colocar um return no corpo da expressão:

printOld(people, p -> {
   p.getAge() >= 80// Erro de compilação
});

Expressão com corpo e sem return.

Predicates

Lambdas trabalham com interfaces que possuem apenas um método e isso é conhecido como interfaces funcionais. Imagine agora que precisamos criar outras interfaces para utilizar com outros tipos de objetos, ex:

public interface 
CheckAnimal {
   boolean isOld(Person p);
}

Poderíamos resolver este problema utilizando generics.

public interface Check<T> {
   boolean isOld(t);
}
E alterando nosso método printOld para utilizar a interface genérica:
static void printOld(List<Person> personList, Check<Person> c) {
   for (Person p : personList) {
       if (c.isOld(p)) {
           System.out.println(p);
       }
   }
}

A chamada para o método continua a mesma:

printOld(people, p ->  p.getAge() >= 80);

Porém ainda precisamos criar uma interface genérica para fazer esta operação. Pensando nisso, a API Java disponibiliza a interface funcional Predicate. Podemos trocar a nossa interface Check pela interface Predicate do Java que está no pacote “java.util.function”. Agora a assinatura do nosso método fica:

static void 
printOld(List<Person> personList, Predicate<Person> c)
O método da interface Predicate que realiza uma verificação é o test, então precisamos alterar também a nossa lógica para chamar o método test.

static void printOld(List<Person> personList, Predicate<Person> c) {
   for (Person p : personList) {
       if (c.test(p)) {
           System.out.println(p);
       }
   }
}

Método printOld alterado para utilizar interface funcional do Java.

A própria API do Java foi alterada para fazer uso dos predicates. A classe ArrayList é um exemplo disso e utiliza no método removeIf:

public boolean 
removeIf(Predicate<? super E> filter)
Vamos utilizar o removeIf em nosso exemplo:

1: List<Person> people = new ArrayList<>();
2: people.add(new Person("Jose"33));
3: people.add(new Person("Ane"90));
4: people.add(new Person("Faria"65));
5: System.out.println(people);
6: people.removeIf(person -> person.getAge() < 80);
7: System.out.println(people);

Na linha 1, criamos um novo Array de Person. Nas linhas 2, 3 e 4 adicionamos pessoas a lista. na linha 4 imprimimos a lista de pessoas:

[Person{name='Jose', age=33}, Person{name='Ane', age=90}, Person{name='Faria', age=65}]

Na linha 6, utilizamos uma expressão lambda para remover todas as pessoas com idade menor que 80. A saída na linha 7 será:

[Person{name='Ane', age=90}]

Muito fácil não é? Para o exame, você só precisa saber como implementar expressões lambda que usam a interface do Predicate. Lembre-se de que a interface Predicate só tem um método e a assinatura dele é:

boolean 
test(t);

Resumo

Para o exame, você precisará ser capaz de:
  • Identificar declarações de método corretas e incorretas
  • Identificar quando um método ou atributo está acessível.
  • Reconhecer usos válidos e inválidos de importações estáticas.
  • Identificar quando chamar métodos estáticos em vez de instâncias.
  • Avaliar código envolvendo construtores.
  • Reconhecer quando uma classe está adequadamente encapsulada.
  • Escrever expressões lambda simples.

Este post foi um tanto extenso e com muitos detalhes novos. Revise o conteúdo e treine bastante código em sua IDE preferida. Caso fique com dúvidas, faça um comentário que vamos ajudá-lo a entender os tópicos abordados. Um abraço e bons estudos!

Comentários

Postar um comentário

Postagens mais visitadas deste blog

Java Records

  Java Records Imutável, Simples e limpa Esta funcionalidade da linguagem apareceu pela primeira vez na versão 14 como experimental e assim continuou até a versão 15 . Agora liberada de forma definitiva no Java 16 . O objetivo é ser possível ter classes que atuam como portadores transparentes de dados imutáveis. Os registros podem ser considerados tuplas nominais. Ou seja, após criado, um record não pode mais ser alterado. Records oferece uma uma sintaxe compacta para declarar classes que são portadores transparentes para dados imutáveis superficiais visando reduzir significamente o detalhamento dessas classes e irá melhorar a capacidade de leitura e manutenção do código. Vamos seguir um exemplo de uma classe chamada Pessoa . O primeiro exemplo vamos utilizar o modo tradicional. public class Pessoa { private String nome; private int idade; public Pessoa (String nome, int idade) { super (); this .nome = nome; this .idade = idade; } public String getNo

Java 8 ao 18: Mudanças mais importantes na plataforma Java

    Vamos rever muitas das mudanças mais importantes na plataforma Java que aconteceram entre a versão 8 (2014) e 18 (2022)   O Java 8 foi lançado em março de 2014 e o Java 18 em março de 2022. São 8 anos de progresso, 203 JEPs (JDK Enhancement Proposals ), entre essas duas versões. Neste post, revisaremos as mudanças mais importantes e discutiremos os benefícios e desafios da adoção de versões mais recentes do JDK para novos aplicativos e para os mais antigos compilados com versões mais antigas. Desde a versão 9, o Java tem novos recursos a cada 6 meses e é muito difícil acompanhar essas novas mudanças. A maioria das informações na internet descreve as mudanças entre as duas últimas versões do Java. No entanto, se você estiver em uma situação semelhante à minha, não está usando uma das versões mais recentes do Java, mas uma das várias versões anteriores (Geralmente 8 ou 11 que são as versões de suporte estendido). Então é útil saber quais novos recursos foram adicionados d

O suporte de longo prazo e o que o LTS significa para o ecossistema Java

A arte do suporte de longo prazo e o que o LTS significa para o ecossistema Java Aqui está o que o Java 17 tem em comum com o Java 11 e o Java 8. Em junho de 2018, há pouco mais de três anos, a Oracle e outros participantes do ecossistema Java anunciaram uma mudança no modelo de cadência de lançamento para Java SE. Em vez de ter um lançamento principal planejado a cada dois ou quatro anos (que geralmente se torna de três a quatro anos), um novo modelo de lançamento de recursos de seis meses seria usado: a cada três anos, um lançamento seria designado como Long-Term Support (LTS) e receba apenas atualizações trimestrais de segurança, estabilidade e desempenho. Esse padrão foi emprestado descaradamente do modelo de lançamento do Mozilla Firefox, mas o ajustou para ficar mais alinhado com os requisitos de uma plataforma de desenvolvimento. A primeira versão do Java lançada sob esse modelo foi o Java SE 11. O lançamento do Java SE 17, o segundo lançamento do LTS sob o novo