Como alguns viram, no último mês de abril foi aniversário da linguagem de programação Java, a linguagem que mais venho utilizando desde meu início de carreira, que está completando 25 anos de idade em 2021.
E para comemorar essa data a Oracle, que atualmente é a detentora da plataforma Java, resolveu criar uma certificação promocional para aqueles que queiram fazer a prova de certificação, e eu resolvi entrar nessa, estudei bastante e consegui passar na prova, onde agora posso dizer que sou um Oracle Certified Professional: Java SE 11 Developer.
Esta prova de certificação é bem famosa por ser uma prova difícil, cheia de pegadinhas e coisas extremamente específicas da linguagem, e quem não estuda bastante os detalhes acaba não tendo êxito, e eu senti muito isso durante meus estudos e por isso vou compartilhar alguns pontos aqui sobre a linguagem que me deixaram bem surpreso.
Atribuições retornam o valor atribuído
Atribuições são operações necessárias e comuns nas linguagens de programação pois obviamente é necessário preencher as variáveis utilizadas no código.
1
2
3
4
5
6
7
int a;
double b;
String c;
a = 10;
b = 50.0;
c = "Hello World";
E eu sempre entendi essas operações como algo void
, ou seja, algo que não retorna um valor, mas eu estava errado, no caso do Java essas atribuições sempre retornam o valor atribuído, que é o valor mais a esquerda, possibilitando fazer o código abaixo.
1
2
3
System.out.println(a = 10);
System.out.println(b = 50.0);
System.out.println(c = "Hello World");
1
2
3
4
Output:
10
50.0
Hello World
Interessante, não? Inclusive podemos ter o seguinte tipo de atribuição, que funciona perfeitamente.
1
2
3
4
int a, b, c, d;
a = b = c = d = 10;
System.out.println(a + b + c + d);
1
2
Output:
40
No código acima só precisamos nos atentar que ele só funciona devido ao fato de declaramos previamente as variáveis e os valores foram atribuídos da direita para esquerda, caso contrário poderíamos ter um erro de utilização de variável não inicializada, se for uma variável local.
1
2
int a, b, c, d;
a = b = c = d; // erro de compilação: Variável d não foi inicializada
Apesar de ser um recurso interessante, utilizá-lo no dia-a-dia acredito não ser uma boa opção pois pode criar um código confuso e fora do padrão de código legível. Acredito que são poucos os lugares em que seu uso seja viável, e uma delas é na leitura de arquivos.
Perceba que no código a seguir a variável line
recebe por atribuição o valor da instrução br.readLine()
e, como esse valor é retornado pela atribuição, ele pode ser verificado se é nulo.
1
2
3
4
5
6
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = br.readLine()) != null) {
//código
}
}
ElseIf
Então, sabe o elseif
que você usa todo dia enquanto programa em Java? Ele não existe! rsrs.
Java realmente não tem o elseif
, mas vou explicar melhor. A linguagem possui a instrução condicional if
, que pode vir acompanhada de um else
.
1
2
3
4
5
6
7
if (condition)
foo();
if (condition)
foo();
else
bar();
Porém existe uma instrução a mais que outras linguagens possuem que é o elseif
, que permite encadear instruções if
e é exatamente essa instrução que não existe em Java.
Vejamos um exemplo de elseif
em Perl, que usa o nome elsif
para a instrução.
1
2
3
4
5
6
7
8
9
10
11
my $a = 1;
if (a == 1) {
# codigo
} elsif (a == 2) {
# codigo
} elsif (a == 3) {
# codigo
} else {
# codigo
}
Em Java o que fazemos é encadear instruções if
usando o else
+ if
, que nada mais é do que if aninhados.
1
2
3
4
5
6
7
8
9
10
11
int a = 1;
if (a == 1) {
//codigo
} else if (a == 2) {
//codigo
} else if (a == 3) {
//codigo
} else {
//codigo
}
Isso na verdade é a mesma coisa que escrever da seguinte forma.
1
2
3
4
5
6
7
8
9
10
11
12
13
if (a == 1) {
//codigo
} else {
if (a == 2) {
//codigo
} else {
if (a == 3) {
//codigo
} else {
//codigo
}
}
}
E no final acaba sendo tudo igual rs.
Operações matemáticas sem ponto flutuante sempre resultam em um inteiro
Qualquer operação realizada envolvendo short
, byte
, char
ou int
, sempre vai retornar um int
.
1
2
3
4
5
6
7
8
9
10
11
12
short a = 1;
byte b = 2;
char c = 3;
int d = 4;
var v1 = a + a;
var v2 = a + b;
var v3 = b + b;
var v4 = b + c;
var v5 = c + c;
var v6 = c + d;
var v7 = a + b + c + d;
No código acima, v1, v2, v3, v4, v5, v6 e v7 são do tipo int
. É importante saber disso pois mesmo operações com um mesmo tipo podem precisar de casting para manter o tipo original. Isso parece acontecer na linguagem para evitar problemas de overflow em tipos menores que int
.
1
2
short a = 1;
short v1 = (short) (a + a);
Para operações com outros tipos de valores o resultado será o maior tipo envolvido.
1
2
3
4
5
6
7
8
9
int a = 1;
long b = 2;
float c = 3.0f;
double d = 4.0;
var v1 = a + b; //v1 é long
var v2 = a + c; //v2 é float
var v3 = a + d; //v3 é double
var v4 = b + d; //v4 é double
Existe divisão por zero
Sim, para valores do tipo ponto flutuante existe divisão por zero, e o resultado é um Infinity
.
1
2
3
4
5
6
7
8
float a = 1;
double b = 2;
var v1 = a / 0;
var v2 = b / 0;
System.out.println(v1 == Float.POSITIVE_INFINITY);
System.out.println(v2 == Double.POSITIVE_INFINITY);
1
2
3
Output:
true
true
Porém para outros tipos de valores a regra matemática ainda persiste, onde a divisão por zero resulta em uma ArithmeticException
.
1
2
int a = 1;
var v1 = a / 0;
1
2
Output:
Exception in thread "main" java.lang.ArithmeticException: / by zero
Se quiser entender um pouco melhor aqui você pode ler a especificação.
Declarações de valores
Durante meus estudos foi possível relembrar e descobrir alguns pontos importante sobre declarações de valores.
Para se declarar valores do tipo float
é sempre necessário que o valor venha acompanhado da letra f
, caso contrário será interpretado como double
.
1
2
float a = 1.0f;
float b = 2.0; //erro de compilação
Se um valor inteiro for declarado com um zero a esquerda ele será interpretado como octal.
1
2
3
4
5
int a = 10;
int b = 010;
System.out.println(a);
System.out.println(b);
1
2
3
Output:
10
8
Para facilitar a declaração de números grandes pode-se utilizar um underline _
na declaração.
1
2
3
final long UM_MILHAO = 1_000_000;
System.out.println(UM_MILHAO == 1000000);
1
2
Output:
true
Bacana, não é mesmo?
Sobrescrita de métodos não precisa ocorrer ao pé da letra
O Java por ser uma linguagem a orientada a objetos possui um dos melhores recursos desse tipo de linguagem que é o polimorfismo, que nos permite trabalhar com abstrações de forma eficiente e fácil, e um dos recursos principais é a possibilidade de sobrescrita de métodos de interfaces, classes abstratas e até mesmo classes concretas, quando é permitido.
E ao sobrescrever esses métodos eu sempre enxerguei o método sobrescrito como um contrato, algo que não pode ser violado e deve ser idêntico quando sobrescrito, porém eu estive errado e é possível sim que a sobrescrita seja diferente, retornando valores e exceções diferenciadas, desde que aderentes ao método sobrescrito.
No caso de exceptions, ao sobrescrever um método que possui uma cláusula throws
nós não precisamos lançar exatamente a mesma exception, podendo ser uma subclasse da mesma.
1
2
3
4
5
6
7
8
9
10
public class SubClass implements SuperClass {
@Override
public void doSomething() throws FileNotFoundException {
//codigo
}
}
public interface SuperClass {
void doSomething() throws IOException;
}
Repare que no exemplo acima a interface possui um método que lança IOException
, mas a classe ao sobrescrever lança FileNotFoundException
que é uma subclasse de IOException
, e isso é permitido.
E o mais legal é que a classe que está sobrescrevendo pode inclusive optar por não lançar nenhuma exception, e isso também é permitido.
1
2
3
4
5
6
7
8
9
10
public class SubClass implements SuperClass {
@Override
public void doSomething() {
//codigo
}
}
public interface SuperClass {
void doSomething() throws IOException;
}
Porém o que não é permitido é lançar uma exception não compatível com a declarada no método sobrescrito, como no caso a seguir onde SQLException
não é compatível com IOException
.
1
2
3
4
5
6
7
8
9
10
public class SubClass implements SuperClass {
@Override
public void doSomething() throws SQLException {
//codigo
}
}
public interface SuperClass {
void doSomething() throws IOException;
}
E no caso de métodos que possuem um retorno nós temos a mesma regra, onde o retorno não precisa ser especificamente o mesmo do método sobrescrito, podendo ser também uma subclasse.
1
2
3
4
5
6
7
8
9
10
public class SubClass implements SuperClass {
@Override
public ByteArrayOutputStream getStream() {
return ...;
}
}
public interface SuperClass {
OutputStream getStream();
}
Apesar da interface definir um OutputStream
como retorno do método getStream()
, a sobrescrita está retornando um ByteArrayOutputStream
.
Interfaces podem ter constantes, métodos privados e métodos default
Esse assunto é relacionado as versões mais recentes da linguagem (da oito em diante) que ganhou vários novos recursos ao longo dos últimos anos, e as interfaces foram as que mais se modernizaram.
E sobre essas mudanças existem três itens que achei bem legal e não sabia que era possível, que é a definição de constantes nas interfaces, métodos privados e métodos default.
Para se declarar uma constante é só declarar uma variável comum.
1
2
3
4
5
6
public interface MyInterface {
String CAR = "Carro";
String MOTORBIKE = "Motocicleta";
}
System.out.println(MyInterface.CAR)
Declarar uma constante em uma interface não necessita do public static final
pois isso já é feito automaticamente pela linguagem.
Uma interface também pode ter métodos privados, que só podem ser acessados dentro da própria interface.
1
2
3
4
5
public interface MyInterface {
private void doSomethingPrivate() {
//codigo
}
}
Os métodos privados só podem ser acessados por métodos do tipo default, que são métodos com implementação que podem ser declarados dentro de uma interface, e são herdados por quem implementar a interface.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyClass implements MyInterface {
@Override
public void doIt() {
doSomething();
}
}
public interface MyInterface {
void doIt();
default void doSomething() {
doSomethingPrivate();
}
private void doSomethingPrivate() {
//codigo
}
}
O uso de métodos privados e default em interfaces ajuda quando existe o caso de diferentes implementações possuírem processos em comum, evitando a duplicação de código ou a criação de uma ‘baseclass’ para reaproveitamento de código.
Sobrescrita de métodos de uma enum
Em um post anterior eu mostrei que enums em Java são bem poderosas e tem uma série de recursos que facilitam muito o desenvolvimento quando se precisa desse tipo de estrutura, onde comentei até que é possível implementar interfaces com uma enum.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum MyEnum implements MyInterface {
FIRST_VALUE {
@Override
public String getMessage() {
return "Primeiro valor.";
}
},
SECOND_VALUE {
@Override
public String getMessage() {
return "Segundo valor.";
}
};
}
public interface MyInterface {
String getMessage();
}
Mas o que eu não sabia era que é possível sobrescrever um método da própria enum, colocando um comportamento personalizado para cada item da enum, se necessário.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum Animal{
DOG("cachorro"),
CAT("gato"),
FISH("peixe") {
@Override
public void action() {
System.out.println("O peixe nada.");
}
};
private String name;
Animal(String name) {
this.name = name;
}
public void action() {
System.out.println("O " + name + " anda.");
}
}
Veja que o item FISH
da enum sobrescreve seu próprio método action()
para adicionar um comportamento específico.
Apesar de esquisito, é um recurso bem interessante.
E esses são os pontos mais interessantes que vi enquanto estudava para a certificação, e tenho certeza que se eu nunca tivesse feito essa prova ia acabar morrendo sem saber rsrs, fica assim a experiência.
Espero que tenham gostado, e até a próxima pessoal.