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.