Declarando floats, por que tipo padrão double?

Estou curioso para saber porque os float liters devem ser declarados da seguinte forma:

float f = 0.1f; 

Ao invés de

 float f = 0.1; 

Por que o tipo padrão é double, por que o compilador não pode inferir que é um float de olhar para o lado esquerdo da tarefa? O Google só exibe explicações sobre quais são os valores padrão, não porque eles são assim.

Por que o tipo padrão é duplo?

Essa é uma pergunta que seria melhor feita aos designers da linguagem Java. Eles são as únicas pessoas que conhecem as verdadeiras razões pelas quais essa decisão de design de linguagem foi tomada. Mas espero que o raciocínio seja algo nas seguintes linhas:

Eles precisavam distinguir entre os dois tipos de literais porque eles realmente significam valores diferentes … de uma perspectiva matemática.

Supondo que eles fizeram “flutuar” o padrão para literais, considere este exemplo

 // (Hypothetical "java" code ... ) double d = 0.1; double d2 = 0.1d; 

Acima, d e d2 teriam valores diferentes. No primeiro caso, um valor de float baixa precisão é convertido em um valor double maior precisão no ponto de atribuição. Mas você não pode recuperar precisão que não está lá.

Eu proponho que um design de linguagem onde essas duas afirmações são legais e significam coisas diferentes é uma idéia ruim … considerando que o significado real da primeira afirmação é diferente do significado “natural”.

Ao fazer do jeito que eles fizeram:

 double d = 0.1f; double d2 = 0.1; 

são legais e significam coisas diferentes novamente. Mas, na primeira afirmação, a intenção do programador é clara e a segunda, o significado “natural”, é o que o programador obtém. E neste caso:

 float f = 0.1f; float f2 = 0.1; // compilation error! 

… o compilador pega a incompatibilidade.


Eu estou supondo que usando floats é a exceção e não a regra (usando duplas em vez disso) com hardware moderno, então em algum momento faria sentido supor que o usuário pretende 0.1f quando ele escreve float f = 0.1;

Eles já podiam fazer isso. Mas o problema está surgindo com um conjunto de regras de conversão de tipo que funcionam … e são simples o suficiente para que você não precise de um diploma em Javaologia para realmente entender. Ter 0.1 significa coisas diferentes em contextos diferentes seria confuso. E considere isto:

 void method(float f) { ... } void method(double d) { ... } // Which overload is called in the following? this.method(1.0); 

O design da linguagem de programação é complicado. Uma mudança em uma área pode ter consequências em outras.


ATUALIZAÇÃO para abordar alguns pontos levantados pelo @supercat.

@supercat: Dadas as sobrecargas acima, qual método será invocado para o método (16777217)? Essa é a melhor escolha?

Eu comentei incorretamente … erro de compilation. Na verdade, a resposta é method(float) .

O JLS diz isso:

15.12.2.5. Escolhendo o método mais específico

Se mais de um método de membro for acessível e aplicável a uma chamada de método, será necessário escolher um para fornecer o descritor para o despacho do método de tempo de execução. A linguagem de programação Java usa a regra de que o método mais específico é escolhido.

[Os símbolos m1 e m2 indicam methods aplicáveis.]

[Se] m2 não é genérico, e m1 e m2 são aplicáveis ​​por invocação estrita ou solta , e onde m1 tem tipos de parâmetro formais S1, …, Sn e m2 tem tipos de parâmetro formais T1, …, Tn, o tipo Si é mais específico que Ti para argumento ei para todo i (1 ≤ i ≤ n, n = k).

As condições acima são as únicas circunstâncias em que um método pode ser mais específico que outro.

Um tipo S é mais específico que um tipo T para qualquer expressão se S <: T (§4.10).

Neste caso, estamos comparando o method(float) e o method(double) que são aplicáveis ​​à chamada. Como float <: double , é mais específico e, portanto, o method(float) será selecionado.

@supercat: Esse comportamento pode causar problemas se, por exemplo, uma expressão como int2 = (int) Math.Round(int1 * 3.5) ou long2 = Math.Round(long1 * 3.5) é substituída por int1 = (int) Math.Round(int2 * 3) ou long2 = Math.Round(long1 * 3)

A mudança pareceria inofensiva, mas as duas primeiras expressões estão corretas até 613566756 ou 2573485501354568 e as duas últimas falham acima de 5592405 [a última sendo completamente falsa acima de 715827882 ].

Se você está falando de uma pessoa fazendo essa mudança … bem, sim.

No entanto, o compilador não fará essa alteração nas suas costas. Por exemplo, int1 * 3.5 tem o tipo double (o int é convertido em double ), então você acaba chamando Math.Round(double) .

Como regra geral, a aritmética Java implicitamente converterá de tipos numéricos “menores” para “maiores”, mas não de “maiores” para “menores”.

No entanto, você ainda precisa ter cuidado, pois (no seu exemplo de arredondamento):

  • o produto de um inteiro e ponto flutuante pode não ser representável com precisão suficiente porque (digamos) um float tem menos bits de precisão que um int .

  • A conversão do resultado de Math.round(double) para um tipo inteiro pode resultar na conversão para o menor / maior valor do tipo inteiro.

Mas tudo isso ilustra que o suporte aritmético em uma linguagem de programação é complicado, e há inevitáveis ​​pegadinhas para um programador novo ou desavisado.

Ha, esta é apenas a ponta do iceberg meu amigo.

Programadores vindos de outras linguagens certamente não se importam de ter que adicionar um pouco de F a um literal comparado a:

 SomeReallyLongClassName x = new SomeReallyLongClassName(); 

Bastante redundante, certo?

É verdade que você teria que conversar com os próprios desenvolvedores Java para obter mais informações. Mas como uma explicação pura do nível de superfície, um conceito importante para entender é o que é uma expressão . Em Java (não sou especialista, portanto, leve isso em conta), acredito que no nível do compilador seu código é analisado em termos de expressões; assim:

 float f 

tem um tipo e

 0.1f 

também tem um tipo ( float ).

De um modo geral, se você for atribuir uma expressão a outra, os tipos devem concordar. Existem alguns casos muito específicos em que essa regra é relaxada (por exemplo, boxear um primitivo como int em um tipo de referência como Integer ); mas, em geral, é válido.

Pode parecer bobo neste caso, mas aqui está um caso muito semelhante em que não parece tão bobo:

 double getDouble() { // some logic to return a double } void example() { float f = getDouble(); } 

Agora, neste caso, podemos ver que faz sentido para o compilador detectar algo errado. O valor retornado por getDouble terá 64 bits de precisão, enquanto f pode conter apenas 32 bits; então, sem um casting explícito, é possível que o programador tenha cometido um erro.

Esses dois cenários são claramente diferentes do ponto de vista humano ; mas meu ponto sobre expressões é que quando o código é primeiro decomposto em expressões e depois analisado, elas são as mesmas.

Tenho certeza de que os autores do compilador poderiam ter escrito alguma lógica não tão inteligente para reinterpretar literais com base nos tipos de expressões aos quais foram atribuídos; eles simplesmente não o fizeram. Provavelmente não foi considerado o esforço em comparação com outros resources.

Por perspectiva, muitas linguagens são capazes de fazer inferências de tipos; em c #, por exemplo, você pode fazer isso:

 var x = new SomeReallyLongClassName(); 

E o tipo de x será inferido pelo compilador com base nessa atribuição.

Para literais, no entanto, C # é o mesmo que Java nesse aspecto.

float tem uma precisão muito pequena e, portanto, uma questão mais interessante é; Por que é apoiado em tudo? Existem situações raras em que float pode ter alguma memory (se você tiver milhões delas) ou você precisa delas para trocar dados com algo que espera flutuar.

Em geral, usar o double é uma escolha melhor, quase tão rápida para PCs modernos, e a economia de memory é menor em comparação com a precisão extra que ela oferece.

Java não olha para o lado esquerdo em nenhuma situação para ver como um valor é usado. Por exemplo, o tipo de retorno de um método não faz parte da assinatura. Em alguns casos, ela será desativada implicitamente para designações de atribuições e de operadores, mas isso geralmente é para manter alguma compatibilidade com C e é uma IMHO ad hoc.