Olá! Na missão Java Syntax Pro, estudamos expressões lambda e dissemos que elas nada mais são do que uma implementação de um método funcional a partir de uma interface funcional. Em outras palavras, esta é a implementação de alguma classe anônima (desconhecida), seu método não realizado. E se nas aulas do curso nos aprofundamos nas manipulações com expressões lambda, agora consideraremos, por assim dizer, o outro lado: a saber, essas mesmas interfaces. A oitava versão do Java introduziu o conceito de interfaces funcionais . O que é isso? Uma interface com um método não implementado (abstrato) é considerada funcional. Muitas interfaces prontas para uso se enquadram nesta definição, como, por exemplo, a interface discutida anteriormente Predicado
Consumidor
Fornecedor
Função
Operador Unário
Comparator
. E também interfaces que nós mesmos criamos, como:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
Temos uma interface cuja tarefa é converter objetos de um tipo em objetos de outro (uma espécie de adaptador). Uma anotação @FunctionalInterface
não é algo supercomplexo ou importante, pois seu objetivo é informar ao compilador que uma determinada interface é funcional e não deve conter mais de um método. Se uma interface com esta anotação tiver mais de um método não implementado (abstrato), o compilador não irá ignorar esta interface, pois a perceberá como um código errado. Interfaces sem essa anotação podem ser consideradas funcionais e funcionarão, mas @FunctionalInterface
isso nada mais é do que um seguro adicional. Vamos voltar para a aula Comparator
. Se você olhar seu código (ou documentação ), verá que ele possui muito mais de um método. Aí você pergunta: como então ela pode ser considerada uma interface funcional? Interfaces abstratas podem ter métodos que não estão dentro do escopo de um único método:
- estático
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
Tendo recebido este método, o compilador não reclamou, o que significa que nossa interface ainda está funcional.
- métodos padrão
default
:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
}
Novamente, vemos que o compilador não começou a reclamar e não ultrapassamos as limitações da interface funcional.
- Métodos de classe de objeto
Object
. Isso não se aplica a interfaces. Mas se tivermos um método abstrato na interface que corresponda à assinatura com algum método da classe Object
, tal método (ou métodos) não quebrará nossa restrição funcional da interface:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
boolean equals(Object obj);
}
E novamente, nosso compilador não reclama, então a interface Converter
ainda é considerada funcional. Agora a questão é: por que precisamos nos limitar a um método não implementado em uma interface funcional? E então para que possamos implementá-lo usando lambdas. Vejamos isso com um exemplo Converter
. Para fazer isso, vamos criar uma classe Dog
:
public class Dog {
String name;
int age;
int weight;
public Dog(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
E um semelhante Raccoon
(guaxinim):
public class Raccoon {
String name;
int age;
int weight;
public Raccoon(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
Suponha que temos um objeto Dog
e precisamos criar um objeto baseado em seus campos Raccoon
. Ou seja, Converter
converte um objeto de um tipo em outro. Como será:
public static void main(String[] args) {
Dog dog = new Dog("Bobbie", 5, 3);
Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);
Raccoon raccoon = converter.convert(dog);
System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
Quando o executamos, obtemos a seguinte saída no console:
Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
E isso significa que nosso método funcionou corretamente.
Interfaces funcionais básicas do Java 8
Bem, agora vamos dar uma olhada em várias interfaces funcionais que o Java 8 nos trouxe e que são usadas ativamente em conjunto com a API Stream.Predicado
Predicate
— uma interface funcional para verificar se uma determinada condição é atendida. Se a condição for atendida, retorna true
, caso contrário - false
:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Por exemplo, considere criar um Predicate
que verifique a paridade de um número do tipo Integer
:
public static void main(String[] args) {
Predicate<Integer> isEvenNumber = x -> x % 2==0;
System.out.println(isEvenNumber.test(4));
System.out.println(isEvenNumber.test(3));
}
Saída do console:
true
false
Consumidor
Consumer
(do inglês - “consumidor”) - uma interface funcional que recebe um objeto do tipo T como argumento de entrada, executa algumas ações, mas não retorna nada:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Como exemplo, considere , cuja tarefa é enviar uma saudação ao console com o argumento de string passado: Consumer
public static void main(String[] args) {
Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
greetings.accept("Elena");
}
Saída do console:
Hello Elena !!!
Fornecedor
Supplier
(do inglês - provedor) - uma interface funcional que não aceita nenhum argumento, mas retorna um objeto do tipo T:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Por exemplo, considere Supplier
, que produzirá nomes aleatórios de uma lista:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList .add("Elena");
nameList .add("John");
nameList .add("Alex");
nameList .add("Jim");
nameList .add("Sara");
Supplier<String> randomName = () -> {
int value = (int)(Math.random() * nameList.size());
return nameList.get(value);
};
System.out.println(randomName.get());
}
E se executarmos isso, veremos resultados aleatórios de uma lista de nomes no console.
Função
Function
— esta interface funcional pega um argumento T e o converte em um objeto do tipo R, que é retornado como resultado:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Como exemplo, tomemos , que converte números do formato de string ( ) para o formato de número ( ): Function
String
Integer
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
}
Quando o executamos, obtemos a seguinte saída no console:
678
PS: se passarmos não apenas números, mas também outros caracteres na string, será lançada uma exceção - NumberFormatException
.
Operador Unário
UnaryOperator
— uma interface funcional que recebe um objeto do tipo T como parâmetro, realiza algumas operações nele e retorna o resultado das operações na forma de um objeto do mesmo tipo T:
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnaryOperator
, que usa seu método apply
para elevar ao quadrado um número:
public static void main(String[] args) {
UnaryOperator<Integer> squareValue = x -> x * x;
System.out.println(squareValue.apply(9));
}
Saída do console:
81
Vimos cinco interfaces funcionais. Isso não é tudo o que está disponível para nós a partir do Java 8 - essas são as interfaces principais. O restante dos disponíveis são análogos complicados. A lista completa pode ser encontrada na documentação oficial da Oracle .
Interfaces funcionais no Stream
Conforme discutido acima, essas interfaces funcionais estão fortemente acopladas à API Stream. Como, você pergunta? E de tal forma que muitos métodosStream
funcionam especificamente com essas interfaces funcionais. Vejamos como as interfaces funcionais podem ser usadas no Stream
.
Método com Predicado
Por exemplo, vamos pegar o método de classeStream
- filter
que toma como argumento Predicate
e retorna Stream
apenas os elementos que satisfazem a condição Predicate
. No contexto de Stream
-a, isso significa que ele passa apenas pelos elementos que são retornados true
quando usados em um método test
de interface Predicate
. Este é o aspecto do nosso exemplo Predicate
, mas para um filtro de elementos em Stream
:
public static void main(String[] args) {
List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.filter(x -> x % 2==0)
.collect(Collectors.toList());
}
Como resultado, a lista evenNumbers
consistirá nos elementos {2, 4, 6, 8}. E, como lembramos, collect
ele reunirá todos os elementos em uma determinada coleção: no nosso caso, em List
.
Método com Consumidor
Um dos métodos doStream
, que utiliza a interface funcional Consumer
, é o peek
. Esta é a aparência do nosso exemplo para Consumer
in Stream
:
public static void main(String[] args) {
List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
.peek(x -> System.out.println("Hello " + x + " !!!"))
.collect(Collectors.toList());
}
Saída do console:
Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
Mas como o método peek
funciona com Consumer
, a modificação das strings in Stream
não ocorrerá, mas peek
retornará Stream
com os elementos originais: os mesmos que vieram até ele. Portanto, a lista peopleGreetings
será composta pelos elementos “Elena”, “John”, “Alex”, “Jim”, “Sara”. Há também um método comumente usado foreach
, que é semelhante ao método peek
, mas a diferença é que ele é terminal final.
Método com Fornecedor
Um exemplo de métodoStream
que usa a interface de função Supplier
é generate
, que gera uma sequência infinita com base na interface de função passada para ele. Vamos usar nosso exemplo Supplier
para imprimir cinco nomes aleatórios no console:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList.add("Elena");
nameList.add("John");
nameList.add("Alex");
nameList.add("Jim");
nameList.add("Sara");
Stream.generate(() -> {
int value = (int) (Math.random() * nameList.size());
return nameList.get(value);
}).limit(5).forEach(System.out::println);
}
E esta é a saída que obtemos no console:
John
Elena
Elena
Elena
Jim
Aqui usamos o método limit(5)
para definir um limite no método generate
, caso contrário o programa imprimiria nomes aleatórios no console indefinidamente.
Método com Função
Um exemplo típico de método comStream
argumento Function
é um método map
que pega elementos de um tipo, faz algo com eles e os repassa, mas estes já podem ser elementos de um tipo diferente. Como seria um exemplo com Function
in Stream
:
public static void main(String[] args) {
List<Integer> values = Stream.of("32", "43", "74", "54", "3")
.map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
Como resultado, obtemos uma lista de números, mas em formato Integer
.
Método com UnaryOperator
Como método que usaUnaryOperator
como argumento, vamos pegar um método de classe Stream
- iterate
. Este método é semelhante ao método generate
: também gera uma sequência infinita, mas possui dois argumentos:
- o primeiro é o elemento a partir do qual começa a geração da sequência;
- o segundo é
UnaryOperator
, que indica o princípio de geração de novos elementos a partir do primeiro elemento.
UnaryOperator
, mas no método iterate
:
public static void main(String[] args) {
Stream.iterate(9, x -> x * x)
.limit(4)
.forEach(System.out::println);
}
Quando o executamos, obtemos a seguinte saída no console:
9
81
6561
43046721
Ou seja, cada um dos nossos elementos é multiplicado por si mesmo e assim por diante para os primeiros quatro números. Isso é tudo! Seria ótimo se depois de ler este artigo você estivesse um passo mais perto de entender e dominar a API Stream em Java!
GO TO FULL VERSION