image

Acesse bootcamps ilimitados e +650 cursos pra sempre

60
%OFF
Article image
Rafael Almeida
Rafael Almeida19/01/2024 01:07
Compartilhe

O erro de 1 bilhão de dólares

  • #Kotlin
  • #Java

Se você sabe programar ou está aprendendo, provavelmente conhece as referências nulas. Você sabia que o criador desse conceito, Tony Hoare, as apelidou de "The billion-dollar mistake"?[1] Nesse artigo, vamos entender melhor o porquê, e também definir de forma mais precisa o que é uma referência nula.

Basicamente, quando um ponteiro não aponta para um objeto válido, ou melhor, ele não aponta para nenhum endereço de memória, dizemos que ele aponta para uma referência nula ou para "null". Pode ser que seja confuso o conceito de ponteiro, mas resumidamente é uma variável que aponta para um endereço de memória, ou seja, armazena um endereço. Na linguagem C definimos ponteiros dessa forma:

#include <stdio.h>

void main(void){
  int * p; /* Esse é um ponteiro que aponta pra um inteiro*/
  int x = 5; /*Essa é uma variável que armazena um inteiro*/
  p = &x; /* & é usado para acessar o endereço de uma variável*/
  printf("%p\n", p); //Printa o conteúdo de p, ou seja, um endereço
  printf("%d\n", *p); //Printa o conteúdo do int para o qual p aponta, ou seja, 5   
}

Talvez seja um pouco confuso o código acima, mas o que fizemos foi definir uma variável p que recebe o endereço de memória de um inteiro, e uma variável x que armazena um valor inteiro. A atribuição "p = &x" não passa o valor 5 para p, mas sim o endereço de memória aonde foi alocada estaticamente a variável x. Se você executar o código acima, verá uma saída parecida com essa:

0x7ffd9927e1cc
5

E para definir uma referência nula, será que basta declarar um ponteiro e não atribuir nada a ele? A resposta é não! Muitas pessoas fazem essa confusão, mas existe uma grande diferença entre referências nulas e variáveis não inicializadas. No C, o compilador não inicializa nada nas variáveis se você simplesmente as declara sem atribuir nada à elas. Veja um exemplo:

#include <stdio.h>


void main(void){
int * p; /*Ponteiro não inicializado*/
printf("%p\n", p);
int * p2 = NULL; /*Referência nula*/
printf("%p", p2);
}

Saída:

0x7ffe2943b278
(nil)

E veja que interessante, não importa quantas vezes você execute esse código, a saída sempre será diferente! Mas então de onde vem esse valor Ox7ffe2943b278? É apenas um lixo de memória. O compilador do C não se dá o trabalho de settar para 0 ou NULL um ponteiro não inicializado, ele recebe o que estiver lá no endereço onde ele será alocado (esse é um dos motivos que faz a linguagem C ser mais rápida).

Mas agora indo para uma linguagem de mais alto nível, como Java, onde entram ponteiros? Então, tirando as variáveis de tipos primitivos, todas as outras são ponteiros. Isso mesmo! A linguagem abstrai essa informação e não precisamos nos preocupar com esse termos, mas quando você faz uma instanciação de uma classe, ou seja, cria um objeto com o uso da palavra reservada new, você está apenas alocando memória suficiente para armazenar aquele objeto daquela classe, e quando você faz a atribuição desse objeto a uma variável do mesmo tipo, você passa o endereço desse objeto para esta variável. Exemplo:

public class Main{
  
  public static void main(String[] args){
      Pessoa pessoa = new Pessoa("José", 80); /*pessoa é um ponteiro que pode fazer referência a objetos da classe Pessoa*/
      System.out.println(pessoa);
  }
}
class Pessoa{
  public String nome;
  public int idade;
  public Pessoa(String nome, int idade){
      this.nome = nome;
      this.idade = idade;
  }
}

A saída desse código é algo parecido com isso:

Pessoa@3b22cdd0

Porque a variável pessoa não armazena o objeto em si, apenas o endereço, e é isso que foi printado. Mas e se não atribuirmos nada à pessoa, pra onde esse ponteiro vai apontar então? No caso de Java, o compilador nem permite essa operação. Se isso fosse permitido, o comportamento do código seria dito indefinido, porque pessoa poderia apontar literalmente para qualquer coisa, já que receberia um lixo de memória. É por isso que o Java te obriga a inicializar, pelo menos, com o valor null, ou seja, uma referência nula. Por exemplo, veja o seguinte código.

public class Main{
  
  public static void main(String[] args){
      Pessoa pessoa;
      System.out.println(pessoa);
  }
}
class Pessoa{
  public String nome;
  public int idade;
  public Pessoa(String nome, int idade){
      this.nome = nome;
      this.idade = idade;
  }
}

Se tentarmos compilar, acontece um erro de compilação.

Main.java:5: error: variable pessoa might not have been initialized
      System.out.println(pessoa);
                         ^
1 error

Mas e aí, fazer Pessoa pessoa = null; resolve todos os problemas? Então, o código compila, mas e se tentarmos, indevidamente, acessar algum campo de pessoa, como nome, o que aconteceria? Na realidade, estaríamos tentando acessar o atributo nome do objeto da classe Pessoa para o qual o ponteiro pessoa aponta. Mas e se ele não aponta pra lugar nenhum, o que acontece? A aplicação quebra, simplesmente. E dispara um NullPointerException. A variável pessoa é um ponteiro, assim como qualquer outra variável que não seja de tipo primitivo em java, e tentar acessar um campo de uma referência nula é um erro gravíssimo.

Veja o exemplo de código.

public class Main{
  
  public static void main(String[] args){
      Pessoa pessoa = null;
      System.out.println(pessoa.nome);
  }
}
class Pessoa{
  public String nome;
  public int idade;
  public Pessoa(String nome, int idade){
      this.nome = nome;
      this.idade = idade;
  }
}

E a exceção gerada.

Exception in thread "main" java.lang.NullPointerException
      at Main.main(Main.java:5)

E nesse exemplo parece muito fácil evitar que isso aconteça, basta fazer a atribuição de um objeto válido para pessoa e pronto. Mas imagine uma aplicação enorme, com milhares de linhas de códigos, e dezenas ou até mesmo centenas de classes. Aí já começa a ficar bem mais perigoso do programa crashar simplesmente por NPE, e olha, isso é comum de acontecer e aconteceu muitas e muitas vezes. Não à toa que Tony Hoare, o criador da referência nula em 1965 para a linguagem ALGOL W, chamou a sua criação de um erro de 1 bilhão de dólares.

Mas e como contornamos esse problema? Será que é só sermos mais atentos e evitarmos a todo custo que isso aconteça? Não parece ser a melhor resposta. Para isso Kotlin tem uma ótima solução, o conceito de Null safety.

A ideia consiste em definir tipos nullable e not nullabe. Por padrão, os tipos são inferidos como non nullable pelo compilador do Kotlin, e ele não permite atribuir null à variáveis desse tipo.[2] Por exemplo, se você simplesmente declarar uma variável do tipo String ou usar a inferência de tipo, essas variáveis serão non nullable.

fun main(){
   var str:String = "str"
   var otherStr = "otherStr"

   //Causariam erro de compilação
   //str = null
   //otherStr = null
}

E se quisermos usar referências nulas no Kotlin, como fazemos? Basta adicionar um ponto de interrogação depois do tipo da variável, e isso deve sempre ser feito explicitamente, pois o compilador sempre vai inferir tipos non nullable.

fun main(){
   var str: String? = null  
}

Tá, e agora você pode se perguntar, aonde que isso resolveu o problema dos NullPointerExceptions? Aí que vem o pulo do gato. Para toda referência nullable, o compilador do Kotlin te obriga a fazer checagens antes de acessar qualquer campo do objeto para o qual a variável faz referência. E se for null, o campo não é acessado. Eu achei isso genial! É uma burocracia a mais, mas muito necessária, e que com certeza evita muitos bugs em muitos programas. Veja um exemplo de como fazer.

fun main() {
  val str: String? = null
  val x: Int = str?.length ?: 0
  println(x)
}

O que aconteceu foi o seguinte, declaramos uma referência nullable do tipo String e atribuímos null a ela. Ao tentar acessar o campo length dessa referência, o uso do ? faz a checagem. E o compilador te obriga a usá-lo. Se str for null, em tempo de execução o campo length nem é acessado. E aí temos também o elvis operator representado por ?:, que funciona como um else. Se str não for null, o campo length é acessado e atribuído a x, caso contrário, é atribuído 0 à x. A saída do programa é essa.

0

Dessa forma, o Kotlin evita a ocorrência de NPE, apesar de existirem situações muito específicas em que isso ainda pode ocorrer, mas são bem incomuns.

E aí, o que você achou? Será que foi realmente um erro Tony Hoare ter criado as referências nulas? Como as coisas teriam sido sem elas? Bilhões de dólares de prejuízo teriam sido evitados? E o que você achou de solução do Kotlin? Deixa nos comentários sua opinião, e se tiver alguma dúvida também sobre os termos abordados, principalmente sobre ponteiros que são bem confusos mesmo, só escrever.

Referências:

  1. Tony Hoare (25/08/2009). "Null References: The Billion Dollar Mistake". Disponível em: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/
  2. kotlinlang (11/09/2023). "Null safety". Disponível em: https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-nullable-types
Compartilhe
Comentários (1)
Julian Gamboa
Julian Gamboa - 19/01/2024 02:01

Obrigado