5 curiosidades da linguagem java que passei a entender após a certificação OCP

Olá!

Sou programador há quase 7 anos e desde o princípio trabalhei com Java. Backend, desktop, mobile, etc. De uma forma ou de outra eu acabava me deparando com java. Fui me afeiçoando com a linguagem e aprendendo a lidar com seus recursos e particularidades.

Ano passado resolvi dar um passo além e tirar minha certificação OCP. Não quero entrar no mérito se vale a pena ou não ter uma certificação Java. Porém, se eu achava que tinha um mínimo de noção da linguagem, tudo foi por água abaixo quando comecei a resolver os simulados e encarar o estilo de questões da prova.

A prova é de multipla escolha e pode apostar que os elaboradores farão de tudo para que essas escolhas lhe deixem o mais confuso possível, utilizando as mais obscuras features e inusitados comportamentos da linguagem para lhe fazer cair no erro.

A seguir eu listo 5 curiosidades sobre a linguagem que aprendi durante os estudos para a certificação. Pode ser que você já conheça todas elas, mas esse post é uma simples forma de compartilhar o que descobri e exercitar uma demanda pessoal latente.

1. Blocos de código podem ser nomeados

Laços de repetições e condicionais são a coluna vertebral de todo programa de computador. A base de qualquer algoritimo. Em java, é possível criar laços e condições junto com o bloco que limita o seu escopo. Quase sempre utilizamos ele de forma "não indetificável". Ex:

while(true) { // Bloco 1
   for(int i = 0; i <= getRecordCount(); i++) { // Bloco 2
      if(hasPaymentToProcess()) { // Bloco 3
          
          if(isCanceled()) { // Bloco 4
             sendEmail();
          }
          
          if(isDone()) { // Bloco 5
              break;
          }
      }
   }
}

Agora se quisermos ter um "identificador" para cada escopo criado, basta definirmo um nome e incluí-lo antes da instrução principal do bloco, seguido por dois pontos. Com isso temos um controle mais fino do fluxo e podemos encerrar a execução do escopo pai diretamente de um escopo filho mais interno Ex:

MAIN_BLOCK: while(true) { // Bloco 1
   ITERATION: for(int i = 0; i <= getRecordCount(); i++) { // Bloco 2
      PAYMENT:if(hasPaymentToProcess()) { // Bloco 3
          
          if(isCanceled()) { // Bloco 4
             sendEmail();
          }
          
          if(isDone()) { // Bloco 5
              break MAIN_BLOCK; // Encerramos a interação do Bloco 1 diretamente do block 5
          }
      }
   }
}

Isso é bastante comum em loops aninhados complexos, onde a decisão tomada em um laço mais interno deve interromper ou continuar a iteração do laço mais externo.

P.S. n° 1: A regra de nomenclatura dos blocos aninhados segue a mesma regra de nomeação de variáveis. P.S. n° 2: Utilizar multiplos blocos aninhados não é algo muito aconselhável e deve ser usado somente em caso realmente necessários.

2. É possível criar um bloco de código em qualquer lugar

Aprendemos que um bloco de código {} serve para delimitar o escopo das coisas e está sempre atrelado a definicação de classes, funcões, laços de repetições e condicionais. Isso é tudo verdade, mas a utilização desse blocos podem ser feitas em qualquer outro contexto. Ex:

public void doSomething() {
    
    {// Criamos um novo escopo
        // Nenhum dos valores declarados aqui será visivel na função
        var a = calculateValue();
        var b = process(a);
        logger.log("Processed value: " + b);
    }
    
    var value = getProcessedValue();
    doVeryComplexComputation(value);
}

Podemos criar esses blocos indepentes a nivel de classe e eles sempre serão executados antes do construtor, na ordem de declaração, semelhante ao que acontece no static {}, porém em um escopo de instância de objeto. Nesse caso os blocos são chamados de inicializadores (Initializer blocks).

Ex:

class MyClass {
 
  private final ObjectManager manager;
  
  public MyClass(){
       // A variável manager já var estar inicializada
       manager.log(this);
  }
 
  {
      manager = new ObjectManager();
  }
  
  {
      manager.initialize();
  }

}

É relativamente comum vermos a utilização desse recurso quando queremos criar um determinado objeto e ao mesmo tempo já definir seu valor inicial. Ex:

Map<String, String> map = new HashMap<>() {{
    put("k1", "value1");
    put("k2", "value2");
    put("k3", "value3");
}};

No trecho acima, implicitamente criamos uma classe anônima que herda de HashMap e através do bloco inicializador temos acesso aos métodos da superclasse. Eu, particulamente, não gosto tanto dessa abordagem devido a criação da subclasse.

3. Classes podem ser definidas dentro de funções

...Ou qualquer outro bloco de codigo. Isso mesmo. É um recurso que dificilmente encontro em códigos de terceiros e nunca senti a necessidade de utilizar nos meus. As Classes locais são definidas diretamente em um bloco de código. Como são classes, podem herdar qualquer outra classe e implementar quantas interfaces forem necessárias. Ex:

public void startNewThread() {

    class MyCallable implements Callable<String> {

        public String call() throws Exception {
            return "DONE";
        }
    }

    class MyRunnable implements Runnable {

        private Callable<String> callable;

        public MyRunnable(Callable<String> callable) {
            this.callable = callable;
        }

        public void run() {
            try {
                callable.call();
            } catch (Exception ex) {
            }
        }
    }

    Thread thread = new Thread(new MyRunnable(new MyCallable()));
    thread.start();

}

4. Qualquer número pode ser um inteiro até que o compilador diga o contrário

Trabalhar com números em uma linguagem de programação é algo que fazemos desde o nosso primeiro hello world. Quem não se lembra, enquanto aprendia algoritimo, de fazer uma calculadora de dois números? Para quem vem de uma linguagem dinâmica como javascript ou python, trabalhar com números em java pode ser uma tortura devido a sua burocrácia de tipos.

Em java temos dois grupos de tipos númericos. Os tipos primitivos e suas representações em classes (boxed types). Por exemplo, podemos representar um número inteiro das seguintes formas:

  • Como um tipo primitivo
int n = 42
  • Como um objeto de uma classe
Integer n = 42;

Além do tipo int e Integer temos short, byte, long, char, float, double e suas representações nas classes Short, Byte, Long, Character, Float e Double.

O valor literal 0 pode ser representado por qualquer tipo primitivo sem nenhuma identifição especial.

short s = 0;
int i = 0;
byte b = 0;
long l = 0;
char c = 0;
float f = 0;
double d = 0;

Isso porque o número zero está presente no conjunto de valores de todos os tipos númericos listados acima. Inclusive no tipo char, onde zero é a representação decimal do char NUL (vazio, nulo).

Essa afirmação é válida durante a declaração estática de variáveis. Pegue essa mesma explicação e tente aplicar no seguinte cenário: uma função com três paramêtros, um do tipo byte, outro do tipo char e por ultimo um do tipo short.

public void calculate(short s, byte b, char c) {...}

calculate(0, 0, 0); // Não irá compilar

Você verá que o código não funciona porque o compilador não consegue garantir a conversão correta do inteiro para os parâmetros, mesmo sendo valores literais.

A coisa fica ainda mais confusa quando tentamos aplicar essa lógica nos tipos númericos não-primitivos

// Funciona
int i = 42;
long l = i;

// Não funciona
Integer i = 42;
Long l = i;

Para o compilador, Integer e Long não são números, mas sim objetos como qualquer outro, cuja única caractéristica em comum é a herança da classe java.lang.Number, que para o compilador também não possui nada de especial.

Para programadores java mais novos, como eu, demora um tempo até enxergarmos as coisas dessa forma. Quando comecei a trabalhar, já existia o recurso de autoboxing, onde podemos atribuir um tipo primitivo para um tipo complexo e a linguagem abstrai isso em tempo de compilação. Se javeiros mais antigos quisesem utilizar os classes de números não-primitivos, eram obrigados a instanciar seus objetos explicitamente. Ex:

Integer n = new Integer(42);

5. Os tipos númericos inteiros não primitivos são cacheados

Como falado no tópico anterior, em java existem duas forma de representação de um número: o tipo primitivo e não primitivo. Os tipos não primitivos são objetos que servem como caixas(boxed types) que armazenam valores mais simples e adicionam funcionalidades e comportamentos à esses tipos básicos.

Comos esses tais tipos não primitivos são objetos, ocupam mais espaço em memória que um número int de 4 bytes ocuparia. Para mitigar esse problema, as classes Integer, Byte, Short, Character e Long possuem recurso de cache e já salvam um quantidade padrão de objetos na memória para serem reutilizados. No caso da classe Integer, são 256. 128 positivos e 128 negativos. Podendo ser alterado via argumentos da VM.

Esse detalhe pode gerar comportamentos estranhos em tempo de execução se utilizarmos o comparador == nesses tipos de objetos. Ex:

Integer i = Integer.valueOf(120);
// Estamos acessando a mesma referencia do objeto acima
Integer i2 = Integer.valueOf("120");

// O resultado será true pois estamos comparando 
// os mesmo objetos em memória
System.out.println(i == i2); //true
Integer i = Integer.valueOf(130);
// Um novo objeto Integer será criado
Integer i2 = Integer.valueOf("130");

// Apresentará false pois estamos comparando 
// referencias diferentes
System.out.println(i == i2); //false

O ideal seria utilizarmos o equals, compareTo ou até mesmo o intValue:

Integer i = Integer.valueOf(130);
Integer i2 = Integer.valueOf("130");

System.out.println(i.equals(i2)); // true
System.out.println(i.compareTo(i2) == 0); // true
System.out.println(i.intValue() == i2.intValue()); // true

Conclusão

Essas foram algumas curiosidades da linguagem/plataforma Java que me deparei durante meus estudos para a certificação. Em aplicações comerciais, dificilmente nos deparamos com tais comportamentos graças a frameworks que abstraem ou melhoram a utilização da linguagem como um todo. Porém, a prova faz questão que entendamos o código em suas minucias, caso contrário estaremos caindo costantemente em pegadinhas de falso-positivo.

O que mais acham interessante na linguagem java ou qualquer outra que gostariam de compartilhar? Fiquem à vontade para comentar. Se falei alguma besteira durante o texto, críticas são sempre bem vindas.

Muito bom. Não sou Javeiro, mas achei pertinente os pontos.

Alguns dos pontos que ele abordou funciona em JavaScirpt/TypeScript também! Achei bacana.
Sim. Algumas coisas realmente dá pra fazer em outras linguagens. A de blocos de instrução e classe dentro de função acho dá pra fazer em JavaScript.

Que conteúdo interessante. Estou vivendo atualmente uma trajetória semelhante, porém ainda estou muito inicio, e por conta disso gostei da forma como você explicou sobre as questões mais técnicas do comportamento da linguagem, por comta disso, gostaria de pedir se poderia me passar alguma referência de estudo, estou achando difícil de encontrar um boa e completa(claro, fora a documentação oficial).

Em java, um bom livro que conta detalhes mais técnicos da plataforma é esse aqui: https://www.amazon.com.br/Java-Efetivo-Melhores-Pr%C3%A1ticas-Plataforma/dp/8550804622/ref=sr_1_2?keywords=java+efetivo&qid=1677073513&sprefix=java+efeti%2Caps%2C287&sr=8-2.
Cara eu estou estudando por um canal no youtube chamado DevDojo, ele possui um curso de java completo, do básico ao avançado. Vale a pena dar uma olhada, estou iniciando na área por esse canal, e estou aprendendo muita coisa! Um abraço, caso eu puder ajudar em algo me chame no linkedin. linkedin.com/in/kaillyarruda
Deparei-me com esse canal e o seu comentário foi como uma espécie de validação, veio em boa hora. Agora sobre o artigo, achei bem detalhado e que agrega bem, ainda mais para iniciantes do "mundo java". Abraço a todos.
O bacana do pessoal do DevDojo é que eles realmente querem gerar valor para as pessoas, eles sempre respondem ás dúvidas e dão dicas do que fazer.

Conteúdo incrível, acabei de aprender isso no curso e realmente você conseguiu resumir isso muito bem rsrs. Parabéns!

Esse tipo de conteúdo agrega muito na carreira, seja Junior, Pleno ou até mesmo Sênior, parabéns pelo post, se possível traga mais conteúdo assim, sei o básico de JAVA, estou estudando C# quem sabe um dia eu traga algo parecido.

Ótimos pontos, eu estudei JAVA por um tempo, lembro que fiz o simulado pra certificação, na época era uns 100 dolares e nao podia repetir, era perdeu, perdeu. Entender qualquer código em suas minuncias é um grande diferencial para um programador.

Olha, eu já tirei essa certificação (quando o Java ainda era da Sun), e digo que muita coisa que caiu na prova eu não usei nunca mais em nenhum projeto. Tinha muita coisa que era tipo "nota de rodapé", aquele detalhezinho obscuro que na prática vc nunca acaba encontrado. Ou coisas que vc descobre rapidamente se estiver na frente do computador (era comum perguntas do tipo "_qual a saída desse código bizarro?_", com códigos tão confusos que ninguém em sã consciência faria em um projeto sério). --- É claro que isso era apenas uma parte da prova, a maioria era de conceitos úteis no dia-a-dia. Aprendi muita coisa útil, várias nuances da linguagem e tal. Só acho que a parte de detalhes obscuros não me acrescentou muito. Mas isso foi há muito tempo, não sei como está a prova hoje.

estudo java, vou até salvar aqui [SALVAISSOAQUIPRAMIM]

Muito interessante, parabéns pelo post