Introdução ao Projeto Amber

Introdução ao Projeto Amber

1. O que é o Projeto Amber

Project Amber is a current initiative from the developers of Java and OpenJDK, aiming to deliver some small but essential changes to the JDK to make the development process nicer. Isso está em andamento desde 2017 e já entregou algumas alterações no Java 10 e 11, com outras agendadas para inclusão no Java 12 e ainda mais em versões futuras.

Essas atualizações são todas empacotadas na forma deJEPs - o esquema de Proposta de Melhoria JDK.

2. Atualizações entregues

Até o momento, o Projeto Amber entregou com sucesso algumas mudanças nas versões atualmente lançadas do JDK -JEP-286eJEP-323.

2.1. Inferência de tipo de variável local

Java 7 introduced the Diamond Operator as a way to make generics easier to work with. Esse recurso significa que não precisamos mais escrever informações genéricas várias vezes na mesma instrução quando estamos definindo variáveis:

List strings = new ArrayList(); // Java 6
List strings = new ArrayList<>(); // Java 7

Java 10 included the completed work on JEP-286, allowing for our Java code do define local variables without needing to duplicate the type information wherever the compiler has it already available. Isso é conhecido na comunidade em geral como a palavra-chavevar e traz funcionalidade semelhante ao Java, conforme está disponível em muitas outras linguagens.

Com este trabalho,whenever we’re defining a local variable, we can use the var keyword instead of the full type definition, e o compilador trabalhará automaticamente com o tipo de informação correto a ser usado:

var strings = new ArrayList();

In the above, the variable strings is determined to be of type ArrayList<String>(), mas sem a necessidade de duplicar a informação na mesma linha.

We can use this anywhere we use local variables, independentemente de como o valor é determinado. Isso inclui tipos e expressões de retorno, bem como atribuições simples como as acima.

A palavravar é um caso especial, pois não é uma palavra reservada. Em vez disso, é um nome de tipo especial. Isso significa que é possível usar a palavra para outras partes do código - incluindo nomes de variáveis. É altamente recomendável não fazer isso para evitar confusão.

We can use local type inference only when we provide an actual type as part of the declaration. Ele é deliberadamente projetado para não funcionar quando o valor é explicitamentenull, quando nenhum valor é fornecido ou quando o valor fornecido não pode determinar um tipo exato - por exemplo, uma definição de Lambda:

var unknownType; // No value provided to infer type from
var nullType = null; // Explicit value provided but it's null
var lambdaType = () -> System.out.println("Lambda"); // Lambda without defining the interface

No entanto,the value can be null if it’s a return value from some other call, pois a própria chamada fornece informações de tipo:

Optional name = Optional.empty();
var nullName = name.orElse(null);

Neste caso,nullName inferirá o tipoString porque é esse o tipo de retorno dename.orElse().

Variables defined this way can have any other modifiers in the same way as any other variable - por exemplo,transitive, synchronized, efinal.

2.2. Inferência de tipo de variável local para lambdas

O trabalho acima nos permite declarar variáveis ​​locais sem precisar duplicar informações de tipo. No entanto, isso não funciona em listas de parâmetros e, em particular, não em parâmetros para funções lambda, o que pode parecer surpreendente.

No Java 10, podemos definir funções do Lambda de uma de duas maneiras - declarando explicitamente os tipos ou omitindo-os completamente:

names.stream()
  .filter(String name -> name.length() > 5)
  .map(name -> name.toUpperCase());

Aqui, a segunda linha tem uma declaração de tipo explícita -String - enquanto a terceira linha a omite completamente e o compilador calcula o tipo correto. What we can’t do is to use the var type here.

Java 11 allows this to happen, então podemos escrever:

names.stream()
  .filter(var name -> name.length() > 5)
  .map(var name -> name.toUpperCase());

This is then consistent with the use of the var type elsewhere in our code.

Os lambdas sempre nos restringiram a usar nomes de tipo completos para cada parâmetro ou para nenhum deles. Isso não mudou, ethe use of var must be for either every parameter or none of them:

numbers.stream()
    .reduce(0, (var a, var b) -> a + b); // Valid

numbers.stream()
    .reduce(0, (var a, b) -> a + b); // Invalid

numbers.stream()
    .reduce(0, (var a, int b) -> a + b); // Invalid

Aqui, o primeiro exemplo é perfeitamente válido - porque os dois parâmetros lambda estão usandovar. The second and third ones are illegal, though, because only one parameter usesvar, embora no terceiro caso também tenhamos um nome de tipo explícito.

3. Atualizações iminentes

Além das atualizações que já estão disponíveis nos JDKs lançados, o próximo lançamento do JDK 12 inclui uma atualização -JEP-325.

3.1. Mudar de expressão

JEP-325 brings support for simplifying the way that switch statements work, and for allowing them to be used as expressions para simplificar ainda mais o código que os utiliza.

Atualmente, a instruçãoswitch funciona de maneira muito semelhante àquelas em linguagens como C ou C ++. These changes make it much more similar to the when statement in Kotlin or the match statement in Scala.

Com essas alterações,the syntax for defining a switch statement looks similar to that of lambdas, com o uso do símbolo. Isso fica entre a correspondência do caso e o código a ser executado:

switch (month) {
    case FEBRUARY -> System.out.println(28);
    case APRIL -> System.out.println(30);
    case JUNE -> System.out.println(30);
    case SEPTEMBER -> System.out.println(30);
    case NOVEMBER -> System.out.println(30);
    default -> System.out.println(31);
}

Note that the break keyword is not needed, and what’s more, we can’t use it here. Está automaticamente implícito que cada correspondência é distinta e a queda não é uma opção. Em vez disso, podemos continuar usando o estilo mais antigo quando precisarmos.

The right-hand side of the arrow must be either an expression, a block, or a throws statement. Qualquer outra coisa é um erro. Isso também resolve o problema de definir variáveis ​​dentro das instruções do switch - que só podem acontecer dentro de um bloco, o que significa que elas têm escopo automático para esse bloco:

switch (month) {
    case FEBRUARY -> {
        int days = 28;
    }
    case APRIL -> {
        int days = 30;
    }
    ....
}

In the older style switch statement, this would be an error because of the duplicate variable *days*. O requisito para usar um bloco evita isso.

The left-hand side of the arrow can be any number of comma-separated values. Isso permite algumas das mesmas funcionalidades do fallthrough, mas apenas para a totalidade de uma correspondência e nunca por acidente:

switch (month) {
    case FEBRUARY -> System.out.println(28);
    case APRIL, JUNE, SEPTEMBER, NOVEMBER -> System.out.println(30);
    default -> System.out.println(31);
}

Até agora, tudo isso é possível com a maneira atual como as instruçõesswitch funcionam e as torna mais organizadas. No entanto,this update also brings the ability to use a switch statement as an expression. Esta é uma mudança significativa para Java, mas é consistente com quantas outras linguagens - incluindo outras linguagens JVM - estão começando a funcionar.

This allows for the switch expression to resolve to a value, and then to use that value in other statements - por exemplo, uma atribuição:

final var days = switch (month) {
    case FEBRUARY -> 28;
    case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
    default -> 31;
}

Aqui, estamos usando uma expressãoswitch para gerar um número e, em seguida, estamos atribuindo esse número diretamente a uma variável.

Before, this was only possible by defining the variable days as null and then assigning it a value inside the switch cases. Isso significava quedays não poderia ser final e poderia potencialmente não ser atribuído se perdêssemos um caso.

4. Próximas Mudanças

Até o momento, todas essas alterações já estão disponíveis ou estarão no próximo lançamento. There are some proposed changes as part of Project Amber that are not yet scheduled for release.

4.1. Literais de cadeia bruta

At present, Java has exactly one way to define a String literal – by surrounding the content in double quotes. É fácil de usar, mas sofre de problemas em casos mais complicados.

Especificamente,it is difficult to write strings that contain certain characters - incluindo, mas não se limitando a: novas linhas, aspas duplas e caracteres de barra invertida. Isso pode ser especialmente problemático em caminhos de arquivo e expressões regulares, onde esses caracteres podem ser mais comuns do que o típico.

JEP-326 introduces a new String literal type called Raw String Literals. Elas estão entre marcas de backtick em vez de aspas duplas e podem conter qualquer caractere dentro delas.

This means that it becomes possible to write strings that span multiple lines, as well as strings that contain quotes or backslashes without needing to escape them. Assim, eles se tornam mais fáceis de ler.

Por exemplo:

// File system path
"C:\\Dev\\file.txt"
`C:\Dev\file.txt`

// Regex
"\\d+\\.\\d\\d"
`\d+\.\d\d`

// Multi-Line
"Hello\nWorld"
`Hello
World`

Em todos os três casos,it’s easier to see what’s going on in the version with the backticks, which is also much less error-prone to type out.

The new Raw String Literals also allow us to include the backticks themselves without complication. O número de crases usados ​​para iniciar e terminar a string pode ser tão longo quanto desejado - não precisa ser apenas uma crase. A corda termina apenas quando atingimos um comprimento igual de reticências. Então, por exemplo:

``This string allows a single "`" because it's wrapped in two backticks``

Isso nos permite digitar as strings exatamente como são, em vez de precisar de sequências especiais para fazer com que certos caracteres funcionem.

4.2. Sobras de lambda

JEP-302 introduz algumas pequenas melhorias na maneira como os lambdas funcionam.

As principais mudanças são na maneira como os parâmetros são manipulados. Em primeiro lugar,this change introduces the ability to use an underscore for an unused parameter so that we aren’t generating names that are not needed. Isso era possível anteriormente, mas apenas para um único parâmetro, pois um sublinhado era um nome válido.

O Java 8 introduziu uma mudança para que usar um sublinhado como nome seja um aviso. O Java 9 então progrediu para se tornar um erro, impedindo-nos de usá-los. Essa mudança futura permite que eles tenham parâmetros lambda sem causar conflitos. Isso permitiria, por exemplo, o seguinte código:

jdbcTemplate.queryForObject("SELECT * FROM users WHERE user_id = 1", (rs, _) -> parseUser(rs))

Sob este aprimoramento,we defined the lambda with two parameters, but only the first is bound to a name. O segundo não é acessível, mas igualmente, nós o escrevemos desta forma porque não temos necessidade de usá-lo.

The other major change in this enhancement is to allow lambda parameters to shadow names from the current context. No momento, isso não é permitido, o que pode nos levar a escrever um código abaixo do ideal. Por exemplo:

String key = computeSomeKey();
map.computeIfAbsent(key, key2 -> key2.length());

There is no real need, apart from the compiler, why key and key2 can’t share a name. O lambda nunca precisa fazer referência à variávelkey, e forçar-nos a fazer isso torna o código mais feio.

Em vez disso, esse aprimoramento nos permite escrevê-lo de uma maneira mais óbvia e simples:

String key = computeSomeKey();
map.computeIfAbsent(key, key -> key.length());

Além disso,there is a proposed change in this enhancement that could affect overload resolution when an overloaded method has a lambda argument. Atualmente, há casos em que isso pode levar à ambiguidade devido às regras sob as quais a resolução de sobrecarga funciona, e esse JEP pode ajustar essas regras um pouco para evitar parte dessa ambiguidade.

Por exemplo,at present, the compiler considers the following methods to be ambiguous:

m(Predicate ps) { ... }
m(Function fss) { ... }

Ambos os métodos usam um lambda com um único parâmetroString e um tipo de retorno não nulo. It is obvious to the developer that they are different – one returns a String, and the other, a boolean, but the compiler will treat these as ambiguous.

Esse JEP pode solucionar essa falha e permitir que essa sobrecarga seja tratada explicitamente.

4.3. Correspondência de padrões

JEP-305 introduz melhorias na maneira como podemos trabalhar com o operadorinstanceof e a coerção de tipo automática.

Atualmente, ao comparar tipos em Java, temos que usar o operadorinstanceof para ver se o valor é do tipo correto e, em seguida, precisamos converter o valor para o tipo correto:

if (obj instanceof String) {
    String s = (String) obj;
    // use s
}

Isso funciona e é compreendido instantaneamente, mas é mais complicado do que o necessário. We have some very obvious repetition in our code, and therefore, a risk of allowing errors to creep in.

This enhancement makes a similar adjustment to the instanceof operator as was previously made under try-with-resources in Java 7. Com essa alteração, a comparação, a conversão e a declaração de variável tornam-se uma única instrução:

if (obj instanceof String s) {
    // use s
}

This gives us a single statement, with no duplication and no risk of errors creeping in, e ainda executa o mesmo que o anterior.

Isso também funcionará corretamente nas ramificações, permitindo o seguinte:

if (obj instanceof String s) {
    // can use s here
} else {
    // can't use s here
}

The enhancement will also work correctly across different scope boundaries as appropriate. A variável declarada pela cláusulainstanceof irá sombrear corretamente as variáveis ​​definidas fora dela, como esperado. Isso acontecerá apenas no bloco apropriado, no entanto:

String s = "Hello";
if (obj instanceof String s) {
    // s refers to obj
} else {
    // s refers to the variable defined before the if statement
}

This also works within the same if clause, da mesma maneira que contamos para verificações denull:

if (obj instanceof String s && s.length() > 5) {
    // s is a String of greater than 5 characters
}

At present, this is planned only for if statements, mas o trabalho futuro provavelmente irá expandi-lo para funcionar comswitch expressions também.

4.4. Corpos de Método Concisos

JEP Draft 8209434is a proposal to support simplified method definitions, de uma forma semelhante ao funcionamento das definições de lambda.

Right now, we can define a Lambda in three different ways: com um corpo, como uma única expressão ou como uma referência de método:

ToIntFunction lenFn = (String s) -> { return s.length(); };
ToIntFunction lenFn = (String s) -> s.length();
ToIntFunction lenFn = String::length;

No entanto,when it comes to writing actual class method bodies, we currently must write them out in full.

This proposal is to support the expression and method reference forms for these methods as well, nos casos em que são aplicáveis. Isso ajudará a manter certos métodos muito mais simples do que atualmente.

Por exemplo, um método getter não precisa de um corpo completo, mas pode ser substituído por uma única expressão:

String getName() -> name;

Da mesma forma, podemos substituir métodos que simplesmente envolvem outros métodos por uma chamada de referência de método, incluindo a passagem de parâmetros entre:

int length(String s) = String::length

These will allow for simpler methods in the cases where they make sense, o que significa que será menos provável que obscureçam a lógica de negócios real no resto da classe.

Observe que isso ainda está no status de rascunho e, como tal, está sujeito a alterações significativas antes da entrega.

5. Enums aprimorados

JEP-301 foi previamente agendado para fazer parte do Projeto Amber. This would’ve brought some improvements to enums, explicitly allowing for individual enum elements to have distinct generic type information.

Por exemplo, permitiria:

enum Primitive {
    INT(Integer.class, 0) {
       int mod(int x, int y) { return x % y; }
       int add(int x, int y) { return x + y; }
    },
    FLOAT(Float.class, 0f)  {
       long add(long x, long y) { return x + y; }
    }, ... ;

    final Class boxClass;
    final X defaultValue;

    Primitive(Class boxClass, X defaultValue) {
       this.boxClass = boxClass;
       this.defaultValue = defaultValue;
    }
}

Infelizmente,experiments of this enhancement inside the Java compiler application have proven that it is less viable than was previously thought. Adicionar informações de tipo genérico a elementos enum tornou impossível usar esses enums como tipos genéricos em outras classes - por exemplo,EnumSet. Isso reduz drasticamente a utilidade do aprimoramento.

Como tal,this enhancement is currently on hold until these details can be worked out.

6. Sumário

Abordamos muitos recursos diferentes aqui. Alguns deles já estão disponíveis, outros estarão disponíveis em breve, e ainda mais estão planejados para lançamentos futuros. Como isso pode melhorar seus projetos atuais e futuros?