Parametrização de Comportamento com Java - Parte II
Lidando com a verbosidade
Parte I deste artigo caso você não tenha lido ;D
Até aqui nosso código está bom, porém sempre é possível melhorar, deste ponto em diante é importantíssimo que conceitos como Classes Anônimas e Polimorfismo estejam bem claros, se esse não é o seu caso ou se você ainda tem dúvidas nesses conceitos não tem problema, pode dar uma olhadinha nesses vídeos:
Universidade XTI - JAVA - 049 - Polimorfismo, Sobrescrita de Métodos
Universidade XTI - JAVA - 050 - Polimorfismo, Classes abstract
Universidade XTI - JAVA - 051 - Polimorfismo, Classes final
Universidade XTI - JAVA - 052 - Polimorfismo, Interfaces
Universidade XTI - JAVA - 098 - Classes Aninhadas e Anônimas
90 - Orientação Objetos - Polimorfismo pt 01 - Introdução
91 - Orientação Objetos - Polimorfismo pt 02 - Funcionamento
92 - Orientação Objetos - Polimorfismo pt 03 - Parâmetros polimórficos
94 - Orientação Objetos - Polimorfismo pt 05 - Programação orientada a interface
191 - Classes Internas pt 03 - Classes Anônimas
Curso Java Completo - Aula 64: Polimorfismo pt 01
Curso Java Completo - Aula 65: Polimorfismo pt 02
Curso Java Completo - Aula 66: Polimorfismo pt 03
Curso de Java 64: Classes aninhadas: internas, locais e anônimas
191 - Classes Internas pt 03 - Classes Anônimas
O problema do código que escrevemos até aqui é que ele ainda é muito verboso, temos de a todo tempo instanciar algo pra usar, fora que temos diversas regras separadas em rotinas diferentes.
Veja que temos que implementar diversas classes, depois instanciar cada uma delas o que gera um overhead completamente desnecessário pois o Java possui um mecanismo chamado de classes anonimas que nos permite delcarar e instanciar uma clase ao mesmo tempo.
Isso nos permite melhorar nosso código tornando ele mais conciso, mas ainda não satisfatório.
Utilizando Classes Anônimas
Uma classe anonima é uma classe comum mas que não tem um nome, essas classes nos permitem declarar e instancia-las ao mesmo tempo, o que nos permite criar implementações ad hoc.
Classes anônimas abordam um pouco a verbosidade associada à declaração de múltiplas classes concretas para uma interface, e isso ainda é insatisfatório para o que estamos buscando pois ainda teremos de criar um objeto e implementar um método explicitamente pra definir um novo comportamento.
Assim sendo, para lidar com esse problema da verbosidade, a partir do Java 8 foram implementadas as expressões lambda.
A boa notícia é que o nosso código está pronto para utilziar expressões lambda, teremos apenas de alterar a chamada:
public class Main {
public static void main(String[] args) {
List<Car> collection = asList(
new Car(Color.RED, "Ferrari 488", "Ferrari", 661),
new Car(Color.YELLOW, "Lamborghini Huracan", "Lamborghini", 602),
new Car(Color.GRAY, "Porsche 911 GT3", "Porsche", 500),
new Car(Color.BLACK, "McLaren 720S", "McLaren", 710),
new Car(Color.RED, "Chevrolet Corvette", "Chevrolet", 495),
new Car(Color.GRAY, "Audi R8", "Audi", 532),
new Car(Color.RED, "Ford GT", "Ford", 660),
new Car(Color.GRAY, "Bugatti Chiron", "Bugatti", 1479),
new Car(Color.YELLOW, "Ferrari LaFerrari", "Ferrari", 950),
new Car(Color.RED, "Lamborghini Aventador", "Lamborghini", 730)
);
List<Car> redCars = CarService.filterCars(collection, (Car car) -> Color.RED.equals(car.color()));
System.out.println(redCars);
CarService.printCar(collection, (Car car) -> car.model() + " is a car of " + car.hp() + " HPs");
}
}
Veja quie nesses exemplo não precisamos mais instanciar os predicados como anteriormente, basta alterar a forma como chamamos os métodos.
Se você não está familiarizado ainda com expressões lambda não se preocupe em entender a sintaxe agora, apenas quero que você saiba que isso existe e que você saiba pra que serve, em breve vou escrever um próximo artigo dedicado especialmente as expressões lambda.
Utilizando conceitos de abstração e generics
Nosso código vem evoluindo bastante, porém veja que as nossas interfaces de predicado ainda são dependentes de um tipo específico
public interface CarPredicate {
boolean test(Car car);
}
Ou seja cada vez que eu tenho um tipo diferente eu tenho de criar uma nova interface, porém o Java possui um feature chamada Generics que nos permite inferir tipos em algumas situações, então vamos fazer o seguinte vamos alterar um pouquinho o nosso predicado e dizer que ele deve esperar um tipo qualquer:
public interface Predicate<T> {
boolean test(T t);
}
Agora não importa mais o tipo que eu passe e eu posso utilizar esse mesmo predicado pra tipos diferentes veja:
public class HighHPCarsPredicate implements Predicate<Car> {
@Override
public boolean test(Car car) {
return car.hp() >= 900;
}
}
public class RedCarOver700HP implements Predicate<Car> {
@Override
public boolean test(Car car) {
return Color.RED.equals(car.color()) && car.hp() >= 700;
}
}
Se no lugar de Car eu tivesse Fruit, ou qualquer outro tipo bastaria criar o predicado passando o tipo, por exemplo:
public class RedCarOver700HP implements Predicate<Fruit> {
@Override
public boolean test(Fruit fruit) {
return Color.RED.equals(fruit.color()) && fruit.weight() >= 45;
}
}
Mas não é só isso, agora que generalizamos a interface vamos para o método que a utiliza:
Veja que no momento temos diversos filtros:
public class CarService {
public static List<Car> filterCarsByColor(List<Car> collection, Color color) {
List<Car> filteredCars = new ArrayList<>();
for (Car car : collection) {
if (color.equals(car.color())) {
filteredCars.add(car);
}
}
return filteredCars;
}
public static List<Car> filterCarsByHP(List<Car> collection, Integer hp) {
List<Car> filteredCars = new ArrayList<>();
for (Car car : collection) {
if (car.hp().equals(hp)) {
filteredCars.add(car);
}
}
return filteredCars;
}
public static List<Car> filterCars(List<Car> collection, CarPredicate predicate) {
List<Car> filteredCars = new ArrayList<>();
for (Car car : collection) {
if (predicate.test(car)) {
filteredCars.add(car);
}
}
return filteredCars;
}
public static void printCar(List<Car> collection, CarFormatter formatter) {
for (Car car : collection) {
String output = formatter.accept(car);
System.out.println(output);
}
}
}
Vamos trocar todos por apenas um chamado filter:
public class CarService {
public static <T> List<T> filter(List<T> collection, Predicate<T> predicate) {
List<T> filtered = new ArrayList<>();
for (T t : collection) {
if (predicate.test(t)) {
filtered.add(t);
}
}
return filtered;
}
public static void printCar(List<Car> collection, CarFormatter formatter) {
for (Car car : collection) {
String output = formatter.accept(car);
System.out.println(output);
}
}
}
Note que utlizamos bastante de generics fazendo com que o método filter aceite qualquer tipo.
Agora podemos utilizar nosso filtro:
public class Main {
public static void main(String[] args) {
List<Car> collection = asList(
new Car(Color.RED, "Ferrari 488", "Ferrari", 661),
new Car(Color.YELLOW, "Lamborghini Huracan", "Lamborghini", 602),
new Car(Color.GRAY, "Porsche 911 GT3", "Porsche", 500),
new Car(Color.BLACK, "McLaren 720S", "McLaren", 710),
new Car(Color.RED, "Chevrolet Corvette", "Chevrolet", 495),
new Car(Color.GRAY, "Audi R8", "Audi", 532),
new Car(Color.RED, "Ford GT", "Ford", 660),
new Car(Color.GRAY, "Bugatti Chiron", "Bugatti", 1479),
new Car(Color.YELLOW, "Ferrari LaFerrari", "Ferrari", 950),
new Car(Color.RED, "Lamborghini Aventador", "Lamborghini", 730)
);
List<Car> redCars = CarService.filter(collection, (Car car) -> Color.RED.equals(car.color()));
System.out.println(redCars);
}
}
Note que o filtro agora funciona com carros e com qualquer outra coisa, por exemplo vamos filtrar os números pares em uma sequência:
public class Main {
public static void main(String[] args) {
List<Integer> numbers = asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
System.out.println(evenNumbers);
}
}
Ordenações
Por fim para fecharmos o assunto de Behavior Parametrization ou Parametrização de Comportamento vamos falar de ordenação.
Vamos dizer que queremos ordenar nossos carros pela potência de cada um, para cumprir essa tarefa vamos utilizar a interface Comparator
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
Então para ordenar os carros pelo HP podemos escrever o seguinte código:
public class Main {
public static void main(String[] args) {
List<Car> collection = asList(
new Car(Color.RED, "Ferrari 488", "Ferrari", 661),
new Car(Color.YELLOW, "Lamborghini Huracan", "Lamborghini", 602),
new Car(Color.GRAY, "Porsche 911 GT3", "Porsche", 500),
new Car(Color.BLACK, "McLaren 720S", "McLaren", 710),
new Car(Color.RED, "Chevrolet Corvette", "Chevrolet", 495),
new Car(Color.GRAY, "Audi R8", "Audi", 532),
new Car(Color.RED, "Ford GT", "Ford", 660),
new Car(Color.GRAY, "Bugatti Chiron", "Bugatti", 1479),
new Car(Color.YELLOW, "Ferrari LaFerrari", "Ferrari", 950),
new Car(Color.RED, "Lamborghini Aventador", "Lamborghini", 730)
);
System.out.println("Unordered: " + collection);
collection.sort(new Comparator<Car>() {
@Override
public int compare(Car o1, Car o2) {
return o1.hp().compareTo(o2.hp());
}
});
System.out.println("Sorted: " + collection);
}
}
Veja que Comparator funciona mais ou menos como o Predicate
que criamos anteriormente, sendo assim podemos utilizar expressões lambda também para reduzir a verbosidade:
public class Main {
public static void main(String[] args) {
List<Car> collection = asList(
new Car(Color.RED, "Ferrari 488", "Ferrari", 661),
new Car(Color.YELLOW, "Lamborghini Huracan", "Lamborghini", 602),
new Car(Color.GRAY, "Porsche 911 GT3", "Porsche", 500),
new Car(Color.BLACK, "McLaren 720S", "McLaren", 710),
new Car(Color.RED, "Chevrolet Corvette", "Chevrolet", 495),
new Car(Color.GRAY, "Audi R8", "Audi", 532),
new Car(Color.RED, "Ford GT", "Ford", 660),
new Car(Color.GRAY, "Bugatti Chiron", "Bugatti", 1479),
new Car(Color.YELLOW, "Ferrari LaFerrari", "Ferrari", 950),
new Car(Color.RED, "Lamborghini Aventador", "Lamborghini", 730)
);
System.out.println("Unordered: " + collection);
collection.sort((Car o1, Car o2) -> o1.hp().compareTo(o2.hp()));
System.out.println("Sorted: " + collection);
}
}
E podemos reduzir ainda mais utilizando o operador ::, chamado pra referênciar métodos - method reference - o qual vamos ver em artigos futuros, nossa chamada ficaria mais ou menos assim:
public class Main {
public static void main(String[] args) {
List<Car> collection = asList(
new Car(Color.RED, "Ferrari 488", "Ferrari", 661),
new Car(Color.YELLOW, "Lamborghini Huracan", "Lamborghini", 602),
new Car(Color.GRAY, "Porsche 911 GT3", "Porsche", 500),
new Car(Color.BLACK, "McLaren 720S", "McLaren", 710),
new Car(Color.RED, "Chevrolet Corvette", "Chevrolet", 495),
new Car(Color.GRAY, "Audi R8", "Audi", 532),
new Car(Color.RED, "Ford GT", "Ford", 660),
new Car(Color.GRAY, "Bugatti Chiron", "Bugatti", 1479),
new Car(Color.YELLOW, "Ferrari LaFerrari", "Ferrari", 950),
new Car(Color.RED, "Lamborghini Aventador", "Lamborghini", 730)
);
System.out.println("Unordered: " + collection);
collection.sort(Comparator.comparing(Car::hp));
System.out.println("Sorted: " + collection);
}
}
Bem mais fácil de ler e entender que estamos comparando o hp dos carros em questão para fazer a ordenação.
Bom pessoal nesse artigo vimos como parametrizar nosso métodos pelo comportamento das nossas aplicações, no próximo vamos falar de expressões lambda.