Pular para o conteúdo principal

Guia da certificação Java SE 8 Programmer l - Parte 6: Orientação a Objetos

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 6 – Orientação a Objetos

Olá. Como estão seus conhecimentos sobre orientação a objetos? Será suficiente para passar na prova? É o que vamos descobrir neste post.

Objetivos do exame

  • Herança
  • Polimorfismo
  • Quando usar cast
  • Usando super e this
  • Classes abstratas e interfaces

Herança

Quando criamos um classe Java, podemos optar por estender de outra classe e assim aproveitas todos os métodos e atributos definidos como public e protected. Por padrão, todas as classes Java estendem de Object. Java suporta apenas herança simples (apenas um extends de outra classe), porém podemos ter vários níveis de herança e implementar várias interfaces.


Níveis de herança. Fonte: https://www.csitquestion.com/java/inheritance-is-a-in-java

Para prevenir que sua classe seja estendida, você pode utilizar a palavra chave final na declaração da classe.
public final class ClassC {
}

Estendendo uma classe

Para estender uma classe, utilizamos a palavra reservada extends.
public class ClassA {
}
public class ClassB extends ClassA {
}
Podemos utilizar os métodos e atributos da superclasse, porem devemos ter cuidado com o escopo dos métodos e variáveis. A classe ClassB pode acessar os atributos e métodos de ClassA que estiverem marcadas como public e protected (default se estiver no mesmo pacote).

Lembra que podemos ter várias classes no mesmo arquivo java? Porém apenas uma pode ser publica e esta deve ter o mesmo nome do arquivo. Isso significa que podemos ter uma hierarquia de classes dentro do mesmo arquivo.
class ClassB extends ClassA {
   void sayHello() {
      System.out.println("hello " + hello);
   }
}
public class ClassA extends Zero {
   protected String hello;
}
class Zero {
}

Todas as nossas classes estendem do Object e Object é a única classe que não estende de nenhuma outra. Acredite, por mais que o compilador não coloque explicitamente a instrução extends Object, é como se ela estivesse lá. Veja o código abaixo:
class Zero extends Object {
}
Com ou sem o extends Object, teremos acesso aos métodos e atributos da classe Object. Isso o Java garante para nós. Todas as classes na plataforma Java são descendentes de Object.

All Classes in the Java Platform are Descendants of Object

Fonte: https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html

Construtores

Sabemos que se não definirmos nenhum construtor em nossa classe, o compilador irá incluir um para nós. Um construtor sem parâmetros. Com a herança, os construtores ficam um pouco mais interessantes e precisamos ter mais atenção no momento de responder alguma pergunta.

Dentro dos construtores, opcionalmente na primeira linha podemos chamar outros construtor através da palavra chave this (para construtores na mesma classe) e super (para construtores da classe pai). No caso do construtor padrão, o Java se encarrega de fazer a chamada ao super.
class ClassB extends ClassA {
   public ClassB() {
      System.out.println("ClassB...");
   }
   public static void main(String[] args) {
      ClassB b = new ClassB();
   }
}
public class ClassA extends Zero {
   public ClassA() {
      System.out.println("ClassA...");
   }
}
class Zero {
   public Zero() {
      System.out.println("Zero...");
   }
}
Ao criar uma instância de ClassB, estamos chamando todos os construtores da hierarquia até chegar no construtor de ClassB. O código acima quando executado, imprime:

Zero...
ClassA...
ClassB...

Agora vamos modificar a classe Zero adicionando um atributo e um novo construtor que inicializa este atributo.
class Zero {
   String init;
   public Zero() {
      System.out.println("Zero...");
   }
   public Zero(String init) {
      System.out.println("Zero init...");
      this.init = init;
   }
}
As subclasses continuam compilando normalmente, mas agora podemos utilizar o segundo construtor para criar nosso objeto já com o atributo preenchido. Vamos alterar a ClassB para chamar o novo construtor.
class ClassB extends ClassA {
   public ClassB() {
      super("ClassB init..."); // Erro de compilação
      System.out.println("ClassB...");
   }
   public static void main(String[] args) {
      ClassB b = new ClassB();
   }
}
Ops! Ao fazer a chamada a super passando uma String como parâmetro, temos um erro de compilação. Isso porque ClassB estende ClassA e não Zero. O super funciona para chamar o construtor da classe acima da hierarquia direta. Então ClassA deve ter este construtor independente se Zero tenha ou não. Alterando ClassA, temos:
public class ClassA extends Zero {
   public ClassA(String init) {
      super(init);
      System.out.println("ClassA init...");
   }
}
Agora ClassB compila sem problemas e ClassA repassa o atributo para Zero através da chamada super(init); 
Executando nosso código, agora temos:

Zero init...
ClassA init...
ClassB...

Perceba que agora temos que chamar o super(init); explicitamente, caso contrário o construtor com parâmetro não será chamado. Experimente remover o código super(init); e executar novamente:

Zero...
ClassA init...
ClassB...

O construtor com parâmetro não foi chamado e a variável init não foi inicializada.

Para ficar claro:
  • Caso não seja definido um construtor, o Java irá providenciar um sem parâmetros.
  • Caso sua classe tenha um construtor padrão sem parâmetros, o Java irá incluir uma chamada super() na primeira linha.
Se você definir uma classe desta forma:
public class Person {
}
Para o Java ela será assim:
public class Person {
   public Person() {
      super();
   }
}
Caso a superclasse não possua um construtor padrão, mas sim um construtor com parâmetros e a classe filha não possuir um construtor compatível, você terá um erro de compilação.
public class ClassA extends Zero {//Classe não compila
class Zero {
   String init;
   public Zero(String init) {
      System.out.println("Zero init...");
      this.init = init;
   }
}
ClassA não compila pois não existe um construtor padrão na classe Zero. Para resolver o problema você precisa implementar um construtor que receba um parâmetro e chame o construtor da superclasse.
public class ClassA extends Zero {
   public ClassA(String init) {
      super(init);
   }
}
Caso omita a chamada super(init); também terá um erro de compilação.

This e Super

Utilizamos this para nos referir a elementos da mesma classe, porém quando herdamos de outra classe também podemos utilizar o this para nos referir aos elementos da super classe caso não tenhamos os mesmo elementos na própria classe:
public class ClassA extends Zero {
   public ClassA(String init) {
      super(init);
   }
   void showInit(){
      System.out.println(this.init);
      System.out.println(super.init);
   }
}
As duas chamadas acima (this.init e super.init) irão se referir a mesma variável da classe Zero, pois não existe um atributo init em ClassA. Caso definirmos este atributo em ClassA, aí sim o this irá se referir a variável da mesma classe e o super a variável da superclasse. Faça um teste e comprove.

Herdando e Substituindo métodos

Fazendo herança de uma classe, garantimos acesso aos membros public de protected da classe pai. Porém podemos definir os mesmos métodos na classe filha, fazendo com que os métodos do pai sejam substituídos (Overriding). Isso é válido quando queremos especializar o comportamento da classe filha.
public class Overriding extends Base {
   public String processMessage(String message) {
      return message.toUpperCase();
   }
}
class Base {
   public String processMessage(String message) {
      return message.toLowerCase();
   }
}
class Test {
   public static void main(String[] args) {
      println(new Base().processMessage("My Message"));
      println(new Overriding().processMessage("My child Message"));
   }
}
A saída do código acima é:
my message
MY CHILD MESSAGE

Podemos reutilizar a implementação da classe pai fazendo uma chamada a super.
public class Overriding extends Base {
   public String processMessage(String message) {
      return super.processMessage(message).concat(" overriding");
   }
}
O que acontece se retirarmos a chamada a super?
public class Overriding extends Base {
   public String processMessage(String message) {
      return processMessage(message).concat(" overriding");
   }
}
Acontece uma chamada recursiva (loop infinito) para o método processMessage da mesma classe, causando uma exceção na execução do código. Exception in thread "main" java.lang.StackOverflowError.
Chamadas recursivas aparecem no exame e você precisa saber identificá-las.

Regras para sobrescrita de métodos

  1. O método na classe filha deve ter a mesma assinatura do método na classe pai.
  2. O método na classe filha deve ser pelo menos tão acessível ou mais acessível que o método na classe pai.
  3. O método na classe filha não pode lançar uma exceção verificada que seja nova ou mais amplo que a classe de qualquer exceção lançada no método de classe pai.
  4. Se o método retornar um valor, ele deverá ser o mesmo ou uma subclasse do método na classe pai, conhecida como tipos de retorno covariantes.
  5. Se o método da classe pai for marcado como estático, o filho também deve ser estático. Porém não é mais considerado uma sobrescrita de método.
Obs: Nenhuma das regras acima se aplica se o método da classe pai for private.

Métodos final

Assim como os atributos podem ser marcados como final, os métodos também podem. Isso garante que quem estender da classe não possa sobrescrever o método.
public class Overriding extends Base {
   public String processMessage(String message) { //Erro
      return message.concat(" overriding");
   }
}
class Base {
   public final String processMessage(String message) {
      return message.toLowerCase();
   }
}
O método processMessage da classe Base foi marcado como final, então não podemos sobrescrever o método em qualquer outra classe que estenda de Base.

Trabalhando com classes abstratas

Quando estamos modelando nosso código, podemos querer criar uma classe apenas para que outras classes estendam dela e reutilizem os membros definidos nela, porém que nunca uma instância desta classe seja criada. Então precisamos de classes abstratas.
public abstract class Person {
   protected String name;
   public String getName(){
      return this.name;
   }
}
Definimos uma classe Person que tem um atributo name e um método getName para recuperar o name; Isso significa que qualquer classe que estenda de Person, terá o atributo name e o método getName():
class Student extends Person {
}
class School {
   public static void main(String[] args) {
      Student student = new Student();
      student.name = "Marcos";
      System.out.println(student.getName());
   }
}
Veja que não criamos nenhum atributo e métodos na classe Student, pois ela herdou da classe Person. E o que acontece se tentarmos criar uma instancia da classe Person?
Person person = new Person(); // Erro de compilação
Não se pode instanciar uma classe abstrata.

Métodos abstratos

Métodos abstratos não possuem corpo, diferente de uma classe abstrata que pode ter atributos e métodos. Métodos abstratos somente podem ser declarados em classes abstratas.
class Student extends Person {
   public abstract void setName(); //Erro de compilação
}
Obs: Uma classe não pode ser definida como final e abstract ao mesmo tempo. O mesmo vale para métodos.
public final abstract class Person {
Error:: illegal combination of modifiers: abstract and final

Quando definimos métodos abstratos na classe pai, todas as classes que estendem desta classe são obrigadas e implementar (sobrescrever) estes métodos.
public abstract class Person {
   abstract String getName();
}
class Student extends Person {
   //Erro de compilação
}
Classe Person não compila pois deve sobrescrever o método abstrato da classe pai. Para que a classe Person pudesse ser compilada, você poderia transformá-la em classe abstrata também.
Regras
  1. Classes abstratas não podem ser instanciadas diretamente.
  2. Classes abstratas podem ser definidas com ou sem métodos abstratos e não abstratos.
  3. As classes abstratas não podem ser marcadas como privadas ou finais.
  4. Uma classe abstrata que estende outra classe abstrata herda todos os seus métodos abstratos como seus próprios métodos abstratos.
  5. A primeira classe concreta que estende uma classe abstrata deve fornecer uma implementação para todos os métodos abstratos herdados.
  6. Métodos abstratos só podem ser definidos em classes abstratas.
  7. Os métodos abstratos não podem ser declarados como privados ou finais.
  8. Os métodos abstratos não devem fornecer um corpo de método (implementação) na classe abstrata para a qual ele é declarado.
  9. Implementar um método abstrato em uma subclasse segue as mesmas regras para sobrescrever um método.

Interfaces

Uma das mudanças no Java 8 foi a introdução de métodos estáticos e default. Os métodos default permitem adicionar novas funcionalidades às interfaces de suas bibliotecas e garantir compatibilidade binária com código escrito para versões mais antigas dessas interfaces.
public interface DefaultInterface {
   void showHello();
   static void hello(){
      System.out.println("hello!");
   }
   default void showDefaultHello(){
      System.out.println("Default Hello!");
   }
}
Interface com um método abstrato, um método estático e um método default. A classe que implementa a interface precisa sobrescrever apenas o método showHello().
class DefaultInterfaceImpl implements DefaultInterface{
   public void showHello() {
      System.out.println("Hello!");
   }
}
A classe DefaultInterfaceImpl herda o método showDefaultHello() que pode ser utilizado.
class MainClass{
   public static void main(String[] args) {
      DefaultInterfaceImpl impl = new DefaultInterfaceImpl();
      impl.showHello();
      impl.showDefaultHello();
      DefaultInterface.hello();
   }
}
Chamando os três métodos disponíveis na interface. Perceba que o método estático não pode ser chamado pela instancia, mas somente pela própria interface. Um tentativa de invocar o método pela instancia resultará em um erro de compilação.

Regras para definição de interfaces

  1. Interfaces não podem ser instanciadas diretamente.
  2. Uma interface não precisa ter nenhum método.
  3. Uma interface não pode ser marcada como final.
  4. Marcar uma interface como privada, protegida ou final ocasionará um erro de compilação
  5. Todos os métodos não default em uma interface são considerados como tendo os modificadores abstratos e públicos em sua definição.

Herança de interface

Assim como uma classe abstrata, uma interface pode ser estendida usando a palavra-chave extend. Dessa maneira, a interface filho herda todos os métodos abstratos da interface pai. Diferentemente de uma classe abstrata, uma interface pode estender várias interfaces.
public interface DefaultInterface extends Cloneable,
 Serializable {
}

Mais regras

Existem questões na prova que tem o objetivo de confundir o candidato ou testar a sua atenção aos detalhes. Sendo assim, você pode encontrar questões com palavras chaves trocadas fazendo com que o código não compile.
public interface DefaultInterface implements Cloneable
Uma interface tentando implementar outra: Erro
Assim como uma classe tentar estender uma interface é um erro.

Implementar várias interfaces que possuem os mesmos métodos é um erro?
interface Bemvindo {
   void sayHello();
}
interface Welcome {
   void sayHello();
}
class Hello implements Bemvindo, Welcome {
   public void sayHello() {
      System.out.println("hello!");
   }
}
Não, pois implementando um dos métodos, significa que atendemos as duas interfaces. Isso vale para métodos compatíveis com número de parâmetros e tipo de retorno. Caso o tipo de retorno fosse diferente, causaria um erro de compilação.

Variáveis

Geralmente utilizamos interface para definir constantes, pois o Java disponibiliza automaticamente as variáveis como public static final.
interface Bemvindo {
   String DEFAULT_HELLO = "Hello!";
}
Não é necessário informar as palavras chave public, static ou final, porém é uma declaração válida. As regras para declaração de variáveis em interfaces são:
  1. Variáveis de interface são consideradas públicas, estáticas e finais. Portanto, marcar uma variável como privada ou protegida acionará um erro do compilador, assim como marcará qualquer variável como abstrata.
  2. O valor de uma variável de interface deve ser definido quando é declarado, uma vez que está marcado como final.

Métodos default

Novidade no Java 8, os métodos default nas interfaces permitem que se tenha uma implementação padrão para o método. Isso garante que adicionando novos métodos em interfaces existentes, não gere erros de compilação em versões anteriores das implementações. Também permite que o método default seja sobrescrito pelas classes que implementam a interface.
public interface HelloProgrammer {
    default void helloWorld(){
        System.out.println("hello world!");
    }
}
Classe que implementa a interface:
public class Programmer implements HelloProgrammer {
}
Veja que não precisamos implementar nenhum método, pois o único método da interface é o método default.
public static void main(String[] args) {
    HelloProgrammer p = new Programmer();
    p.helloWorld();
}
Utilize o método helloWorld normalmente.

Regras para o uso de métodos default em interfaces:
  1. Um método default só pode ser declarado dentro de uma interface e não dentro de uma classe ou classe abstrata
  2. Um método default deve ser marcado com a palavra-chave default e se estiver marcado como default, ele deve fornecer um corpo de método.
  3. Um método default não é considerado estático, final ou abstrato, pois pode ser usado ou sobrescrito por uma classe que implementa a interface.
  4. Como todos os métodos em uma interface, um método default é considerado público e não será compilado se marcado como privado ou protegido.

Herança e sobrescrita

É possível seguir as regras de herança e sobrescrita para os métodos default nas interfaces. Uma interface pode definir um método abstrato e a interface que estende pode implementar através de um método default.
public interface Hello {
    void helloWorld();
}
public interface HelloProgrammer extends Hello {
    default void helloWorld(){
        System.out.println("hello world!");
    }
}
A interface HelloProgrammer esta sobrescrevendo o método helloWorld fornecendo uma implementação padrão. Agora a situação contrária onde uma interface estende outra que possui um método padrão. 
public interface Hello {
    default void helloWorld(){
        System.out.println("hello!");
    }
}
public interface HelloProgrammer extends Hello {
}
Aqui, a interface HelloProgrammer pode sobrescrever o método helloWorld ou até transformá-lo em abstrato, forçando a implementação do método.
public interface HelloProgrammer extends Hello {
    void helloWorld();
}
Agora voltamos a uma questão de que Java não permite herança múltipla. Não permitia até a criação dos métodos default. Como uma interface pode estender de mais de uma interface, agora temos um problema.
public interface Hello {
    default void helloWorld(){
        System.out.println("hello!");
    }
}
public interface World {
    default void helloWorld() {
        System.out.println("hello!");
    }
}
public interface HelloProgrammer extends Hello, World {
 // Não compila
}
O Java não sabe qual método usar quando helloWorld for invocado. Por isso é obrigatório sobrescrever o método ou deixá-lo abstrato. O mesmo ocorre se você tentar implementar duas interfaces que tem os mesmos métodos default.
public class Programmer implements Hello, World {
    // Não compila
}

Métodos estáticos

É possível também declarar métodos estáticos nas interfaces onde a diferença para métodos estáticos de classes é que nenhuma implementação irá herdar os métodos. Eles ficam restritos a própria interface.
public interface Hello {
    default void helloWorld(){
        System.out.println(getHello());
    }
    static String getHello(){
        return "hello";
    }
}
Alteramos a nossa interface Hello para fornecer um método estático. Este método ficará acessível em nível público:
  1. Como todos os métodos em uma interface, um método estático é considerado público e não será compilado se marcado como privado ou protegido.
  2. Para se referenciar ao método estático, uma referência ao nome da interface deve ser usada.
public interface World {
    default void helloWorld() {
        System.out.println(Hello.getHello());
    }
}
Obs: No caso dos métodos estáticos, não teremos problemas com herança múltipla, pois os métodos estáticos não são herdados e devem ser invocados utilizando o nome da interface.

Polimorfismo

Múltiplas formas. Em Java significa que podemos ter uma uma única instancia de objeto e várias formas de acessá-lo. Para que isso seja possível, utilizamos herança e interfaces.
public interface HelloProgrammer {
    void sayHello();
}
public abstract class Person {
    String name;
}
public class Programmer extends Person implements HelloProgrammer {
    public void sayHello() {
        System.out.println("Hello Java!!");
    }
    public static void main(String[] args) {
      1 - Programmer programmer = new Programmer();
      2 - programmer.name = "Steve";
      3 - programmer.sayHello();

      4 - Person steve = programmer;
      5 - System.out.println(steve.name);

      6 - HelloProgrammer hello = programmer;
      7 - hello.sayHello();
    }
}
Perceba que criamos um objeto do tipo Programmer na linha 1 do nosso exemplo. Na linha 4, associamos o objeto programmer ao tipo Person (Programmer é um Person) e imprimimos o nome. Na linha 6, associamos programmer para o tipo HelloProgrammer (Programmer implementa HelloProgrammer) e invocamos o método sayHello().
Após o objeto ter sido atribuído a um novo tipo de referência, somente os métodos e variáveis disponíveis para esse tipo de referência poderão ser chamados no objeto sem uma conversão explícita (cast). Ex:
steve.sayHello(); //não compila
hello.name; //não compila
Podemos dizer que o principio de polimorfismo segue duas regras:
  1. O tipo do objeto (instância) determina quais propriedades existem dentro do objeto na memória.
  2. O tipo de referência (variável) do objeto determina quais métodos e variáveis são acessíveis.

Cast de objetos

Para retornarmos a referência para o tipo original (mais amplo), utilizamos o cast. Este processo é necessário para deixar explicito que queremos mudar a referencia de um tipo com o escopo mais limitado para um tipo mais amplo. Quando transformamos o Programmer em Person, limitamos o escopo para o tipo Person. Para fazer o processo reverso vamos precisar usar o cast:
programmer = (Programmer) steve;
programmer.sayHello();
Colocamos o tipo entre parenteses antes da variável com menor escopo. Caso tentarmos atribuir sem o cast, teremos um erro de compilação.

Regras

  1. Atribuir um objeto de uma subclasse para uma superclasse não requer uma conversão explícita.
  2. Atribuir um objeto de uma superclasse para uma subclasse requer uma conversão explícita.
  3. O código não irá compilar conversões para tipos não relacionados.
  4. Mesmo se o código compilar sem problemas, uma exceção pode ser lançada em tempo de execução se o objeto sendo convertido não for realmente uma instância dessa classe.
Preste atenção quando tiver uma operação de cast e a instância do objeto for de menor escopo. O código irá compilar, porém uma exceção irá ocorrer em tempo de execução.
Person bill = new Person();
programmer = (Programmer) bill;
programmer.sayHello();
Exception in thread "main" java.lang.ClassCastException: Person cannot be cast to Programmer 
*Mensagem de erro resumida

Resumo

Este post foi uma revisão aos conceitos de herança o polimorfismo exigidos na prova. Pratique escrevendo código e testando as possibilidades. Qualquer dúvida ou problema, deixe um comentário. Um abraço e bons estudos!





Comentários

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