Conteúdo


Linguagens Java 8

O Java conhece seu tipo

Saiba como usar a inferência de tipo em expressões lambda e obtenha dicas para melhorar a nomenclatura de parâmetros

Comments

Conteúdos da série:

Esse conteúdo é a parte # de # na série: Linguagens Java 8

Fique ligado em conteúdos adicionais dessa série.

Esse conteúdo é parte da série:Linguagens Java 8

Fique ligado em conteúdos adicionais dessa série.

Java™ 8 é a primeira versão do Java que suporta inferência de tipo e somente para expressões lambda. O uso da inferência de tipo em lambdas é muito eficiente e será uma base para as versões futuras do Java, nas quais a inferência de tipo será incluída para variáveis e, provavelmente, muito mais. O truque é nomear bem os parâmetros e confiar no compilador Java para inferir o restante.

Geralmente, o compilador é mais do que capaz de inferir tipos. E quando não for, ele reclamará.

Saiba como a inferência de tipo funciona em expressões lambda e veja pelo menos um exemplo em que ela falha. Mesmo em caso de falha, existe uma solução alternativa.

Tipos explícitos e redundância

Suponhamos que você pergunte a alguém, "Qual é seu nome?" e a pessoa responda "Meu nome é John". Essa resposta ocorre bastante, mas seria mais eficiente se a pessoa simplesmente dissesse "John." Você precisa apenas do nome, portanto, o restante da frase é ruído.

Infelizmente, nós fazemos esse tipo de coisa o tempo todo no código. Aqui está como um desenvolvedor Java pode usar forEach para iterar e imprimir o dobro de cada valor em um intervalo:

IntStream.rangeClosed(1, 5)
  .forEach((int number) -> System.out.println(number * 2));

O método rangeClosed produz um fluxo de valores int de 1 a 5. A expressão lambda, em pleno vigor, recebe um parâmetro int chamado iOS e usa o método println de PrintStream para imprimir o dobro desse valor. Sintaticamente não há nada de errado com a expressão lambda, mas o detalhe de tipo é redundante.

Inferência de tipo no Java 8

Quando você extrai um valor de um intervalo de números, o compilador sabe que o tipo do valor é int. Não há necessidade de indicar o valor explicitamente no código, embora essa tenha sido a convenção até agora.

No Java 8, é possível remover o tipo da expressão lambda, como é mostrado aqui:

IntStream.rangeClosed(1, 5)
  .forEach((number) -> System.out.println(number * 2));

Por ter tipos estáticos, o Java precisa conhecer os tipos de todos os objetos e variáveis no tempo de compilação. Omitir o tipo na lista de parâmetros de uma expressão lambda não faz com que o Java seja uma linguagem de tipos dinâmicos. No entanto, a inclusão de uma dose saudável de inferência de tipo aproxima o Java de outras linguagens de tipo estático, como Scala ou Haskell.

Confie no compilador

Se você omitir o tipo em um parâmetro de uma expressão lambda, o Java exigirá detalhes contextuais para inferir esse tipo.

Retornando ao exemplo anterior, quando chamamos forEach em IntStream, o compilador consulta esse método para determinar os parâmetros que ele usa. O método forEach de IntStream espera a interface funcional IntConsumer, cujo método abstrato accept usa um parâmetro do tipo int e retorna void.

Se você especificar o tipo na lista de parâmetros, o compilador confirmará que o tipo é o esperado.

Se você omitir o tipo, o compilador inferirá o tipo esperado —int neste caso.

Se você fornecer o tipo ou o compilador o inferir, o Java saberá o tipo de seu parâmetro de expressão lambda no tempo de compilação. É possível testar isso cometendo um erro dentro de uma expressão lambda e omitindo o tipo do parâmetro:

IntStream.rangeClosed(1, 5)
  .forEach((number) -> System.out.println(number.length() * 2));

Ao compilar esse código, o compilador Java retornará o erro a seguir:

Sample.java:7: error: int cannot be dereferenced
      .forEach((number) -> System.out.println(number.length() * 2));
                                                    ^
1 error

O compilador sabe o tipo do parâmetro chamado iOS. Ele reclamou porque não é possível retirar a referência de uma variável do tipo int usando o operador dot. Isso pode ser feito para objetos, mas não para int .

Benefícios da inferência de tipo

Há dois grandes benefícios de omitir o tipo em expressões lambda:

  • Menos digitação. Não há necessidade de inserir as informações de tipo porque o compilador pode determiná-las facilmente sozinho.
  • Menos ruído no código—(number) é muito mais simples do que (int number).

Além disso, como regra, se há apenas um parâmetro, omitir o tipo significa que também é possível omitir o (), desta forma:

IntStream.rangeClosed(1, 5)
  .forEach(number -> System.out.println(number * 2));

Observe que será necessário incluir os parênteses em expressões lambda que usarem mais de um parâmetro.

Inferência de tipo de capacidade de leitura

A inferência de tipo em expressões lambda é um desvio da prática normal de Java, na qual especificamos o tipo de cada variável e parâmetro. Embora alguns desenvolvedores concordem que a convenção de Java de especificar o tipo aumenta a capacidade de leitura e facilita o entendimento, eu acredito que a preferência possa refletir mais o hábito do que a necessidade.

Use um exemplo de um pipeline de função com uma série de transformações:

List<String> result = 
  cars.stream()
    .map((Car c) -> c.getRegistration())
    .map((String s) -> DMVRecords.getOwner(s))
    .map((Person o) -> o.getName())
    .map((String s) -> s.toUpperCase())
    .collect(toList());

Aqui, vamos começar com uma coleção de instâncias de Carro e informações de registro associadas. Vamos obter o proprietário de cada carro e o nome do proprietário, que vamos converter em maiúsculas. Finalmente, colocaremos os resultados em uma lista.

Todas as expressões lambda neste código têm um tipo especificado para seu parâmetro, mas usamos nomes de variável de uma única letra para os parâmetros. Isso é muito comum em Java. Mas também é ruim porque não inclui o contexto específico do domínio.

É possível fazer isso de uma maneira melhor. Vamos ver o que ocorre quando reescrevemos o código com nomes de parâmetro mais fortes:

List<String> result = 
  cars.stream()
    .map((Car car) -> car.getRegistration())
    .map((String registration) -> DMVRecords.getOwner(registration))
    .map((Person owner) -> owner.getName())
    .map((String name) -> name.toUpperCase())
    .collect(toList());

Esses nomes de parâmetros contêm informações específicas do domínio. Em vez de usar s para representar uma Sequência, especificamos detalhes específicos do domínio, como registration e name. Dessa forma, em vez de p ou o, usamos o owner para mostrar que Person não é apenas uma pessoa, mas também é o proprietário do carro.

Cada expressão lambda nesse exemplo é um entalhe melhor do que aquele que substitui. Quando lemos a lambda—por exemplo, (Person owner) -> owner.getName()—sabemos que estamos obtendo o nome do proprietário e não apenas um nome de uma pessoa qualquer.

Parâmetros de nomenclatura

Algumas linguagens como Scala e TypeScript dão mais importância aos nomes de parâmetros do que aos seus tipos. Em Scala, definimos os parâmetros antes do tipo, por exemplo, escrevendo:

def getOwner(registration: String)

em vez de:

def getOwner(String registration)

Tanto o tipo quanto os nomes de parâmetros são úteis, mas em Scala os nomes de parâmetros são mais importantes. Também é possível aplicar essa ideia ao escrever expressões lambda em Java. Observe o que ocorre quando descartamos os detalhes de tipo e os parênteses do exemplo de registro de carro em Java:

List<String> result = 
  cars.stream()
    .map(car -> car.getRegistration())
    .map(registration -> DMVRecords.getOwner(registration))
    .map(owner -> owner.getName())
    .map(name -> name.toUpperCase())
    .collect(toList());

Como incluímos nomes de parâmetro descritivos, não perdemos muito contexto, e o tipo explícito, agora redundante, desapareceu silenciosamente. O resultado é um código mais limpo e claro.

Limites de inferência de tipo

Embora o uso da inferência de tipo traga benefícios de eficiência e capacidade de leitura, ele não é uma técnica para todas as ocasiões. Em alguns casos, simplesmente não é possível usar a inferência de tipo. Felizmente, é possível contar com o compilador Java para informá-lo quando isso ocorre.

Primeiro vamos analisar um exemplo em que o compilador é testado com sucesso e, em seguida, outro em que ele falha. O mais importante é que nos dois casos é possível contar com o compilador para trabalhar da maneira certa.

Expandindo a inferência de tipo

Para o nosso primeiro exemplo, suponhamos que queremos criar um Comparator para comparar as instâncias de Carro. A primeira coisa que precisamos é de um Carro :

class Car {
  public String getRegistration() { return null; }
}

Em seguida, vamos criar um Comparator que compare as instâncias Carro com base em suas informações de registro:

public static Comparator<Car> createComparator() {
  return comparing((Car car) -> car.getRegistration());
}

A expressão lambda usada como argumento para o método comparing contém as informações de tipo em sua lista de parâmetros. Sabemos que o compilador Java é muito perspicaz sobre a inferência de tipo, então vamos ver o que ocorre ao omitir o tipo de parâmetro, desta forma:

public static Comparator<Car> createComparator() {
  return comparing(car -> car.getRegistration());
}

O método comparing usa um argumento. Ele espera Function<? super T, ? extends U> e retorna Comparator<T>. Como comparing é um método estático no Comparator<T>, até agora, o compilador não tem nenhuma pista do que T ou U pode ser.

Para resolver isso, ele expande um pouco mais a inferência, para além do argumento passado para comparing . Ele procura o que estamos fazendo com o resultado da chamada de comparing. Com isso, o compilador determina que nós simplesmente retornamos o resultado. Em seguida, ele vê que o Comparator<T, que é retornado por comparing, é retornado como Comparator<Car> por createComparator.

Pronto! Agora o compilador está nos acompanhando: ele infere que T deve ser ligado a Carro. Com isso, ele sabe que o tipo do parâmetro car na expressão lambda deve ser Carro.

O compilador precisou fazer um trabalho extra para inferir o tipo neste caso, mas obteve sucesso. Em seguida, vamos ver o que acontece quando elevamos o desafio e chegamos aos limites do que o compilador pode fazer.

Limites da inferência

Para começar, vamos incluir uma nova chamada após a anterior para comparing. Nesse caso, também introduzimos novamente o tipo explícito para o parâmetro da expressão lambda:

public static Comparator<Car> createComparator() {
  return comparing((Car car) -> car.getRegistration()).reversed();
}

Esse código não tem problemas para compilar com o tipo explícito, mas agora vamos eliminar as informações de tipo e ver o que acontece:

public static Comparator<Car> createComparator() {
  return comparing(car -> car.getRegistration()).reversed();
}

Como você pode ver abaixo, ele não funciona bem. O compilador Java reclama com erro:

Sample.java:21: error: cannot find symbol
    return comparing(car -> car.getRegistration()).reversed();
                               ^
  symbol:   method getRegistration()
  location: variable car of type Object
Sample.java:21: error: incompatible types: Comparator<Object> cannot be converted to Comparator<Car>
    return comparing(car -> car.getRegistration()).reversed();
                                                           ^
2 errors

Assim como o cenário anterior, antes da inclusão de .reversed(), o compilador pergunta o que estamos fazendo com o resultado da chamada para comparing(car -> car.getRegistration()). No caso anterior, nós retornamos o resultado como Comparable<Car> e, assim, o compilador pôde inferir que o tipo de Tseria Carro.

No entanto, nesta versão modificada, passamos o resultado de comparable como um destino para chamar reversed(). O programa comparable retorna Comparable<T>e reversed() não revela nada mais sobre o que T pode ser. Com isso, o compilador infere que o tipo de Tdeve ser Object. Infelizmente, não há o suficiente para esse código, pois Object não tem o método getRegistration() que estávamos chamando na expressão lambda.

A inferência de tipo falha nesse momento. Nesse caso, o compilador realmente precisou de informações. A inferência de tipo considera os argumentos e os elementos de retorno ou de designação para determinar o tipo, mas se o contexto não oferecer detalhes suficientes, o compilador chegará ao seu limite.

Referências de método como solução?

Antes de desistirmos dessa situação específica, vamos experimentar mais uma coisa: em vez de uma expressão lambda, vamos tentar usar uma referência de método:

public static Comparator<Car> createComparator() {
  return comparing(Car::getRegistration).reversed();
}

O compilador ficou muito satisfeito com essa solução. Ele usa o Car:: na referência de método para inferir o tipo.

Conclusão

O Java 8 introduziu a inferência de tipo limitada para parâmetros em expressões lambda e a inferência de tipo será expandida para variáveis locais em uma versão futura do Java. Aprender a omitir os detalhes de tipo e a confiar no compilador o ajudará a formar a base para o que vem depois.

Confiar na inferência de tipo e em parâmetros nomeados corretamente o ajudará a escrever um código conciso, mais expressivo e com menos ruído. Use a inferência de tipo sempre que você achar que o compilador será capaz de inferir o tipo sozinho. Forneça os detalhes de tipo somente em situações em que você tiver certeza de que o compilador realmente precisa de ajuda.


Recursos para download


Temas relacionados


Comentários

Acesse ou registre-se para adicionar e acompanhar os comentários.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=1057956
ArticleTitle=Linguagens Java 8: O Java conhece seu tipo
publish-date=02082018