Por que o Java possui primitivas para números de tamanhos diferentes?

20

Em Java há tipos primitivos para byte, short, inte longe a mesma coisa para floate double. Por que é necessário que uma pessoa defina quantos bytes devem ser usados ​​para um valor primitivo? O tamanho não podia ser determinado dinamicamente, dependendo do tamanho do número passado?

Existem 2 razões pelas quais posso pensar:

  1. Definir dinamicamente o tamanho dos dados significaria que também seria necessário alterar dinamicamente. Isso pode causar problemas de desempenho?
  2. Talvez o programador não queira que alguém possa usar um número maior que um determinado tamanho e isso permita que ele o limite.

Eu ainda acho que poderia ter havido muito a ganhar com o simples uso de um único inte floattipo. Houve algum motivo específico para o Java ter decidido não seguir esse caminho?

yitzih
fonte
4
Para os que recusam, eu acrescentaria que esta pergunta está conectada a uma pergunta que os pesquisadores do compilador estão procurando responder .
rwong
Então, se você adicionou a um número, acha que o tipo deve ser alterado dinamicamente? Eu quero mesmo o tipo alterado? Se o número for inicializado como intDesconhecido alfa = a + b; você entendeu que seria um pouco difícil para o compilador. Por que isso é específico para java?
paparazzo
@Paparazzi Existem linguagens de programação e ambientes de execução existentes (compiladores, intérpretes, etc.) que armazenam o número inteiro de largura dinâmica com base no tamanho do valor real (por exemplo, o resultado da operação de adição). As conseqüências são as seguintes: o código a ser executado na CPU se torna mais complicado; o tamanho desse inteiro se torna dinâmico; a leitura de um número inteiro de largura dinâmica da memória pode exigir mais de um disparo; estruturas (objetos) e matrizes que contêm números inteiros de largura dinâmica dentro de seus campos / elementos também podem ter tamanho dinâmico.
rwong
1
@ tofro eu não entendo. Basta enviar o número no formato que desejar: decimal, binário, etc. Serialização é uma preocupação completamente ortogonal.
gardenhead 25/09/16
1
@gardenhead É ortogonal, sim, mas ... considere o caso em que você deseja se comunicar entre um servidor escrito em Java e um cliente escrito em C. É claro que isso pode ser resolvido com uma infraestrutura dedicada. Por exemplo, existem coisas como developers.google.com/protocol-buffers . Mas essa é uma grande marreta para a pequena porca de transferir um número inteiro pela rede. (Eu sei, este não é um argumento forte aqui, mas talvez um ponto a considerar - discutir os detalhes está além do escopo dos comentários).
precisa saber é o seguinte

Respostas:

16

Como muitos aspectos do design de linguagem, trata-se de uma troca de elegância e desempenho (para não mencionar alguma influência histórica de idiomas anteriores).

Alternativas

Certamente é possível (e bastante simples) criar uma linguagem de programação que possua apenas um único tipo de números naturais nat. Quase todas as linguagens de programação usadas para estudos acadêmicos (por exemplo, PCF, Sistema F) possuem esse tipo de número único, que é a solução mais elegante, como você supôs. Mas o design de linguagem na prática não se trata apenas de elegância; também devemos considerar o desempenho (a extensão em que o desempenho é considerado depende da aplicação pretendida do idioma). O desempenho compreende restrições de tempo e espaço.

Restrições de espaço

Deixar o programador escolher o número de bytes antecipadamente pode economizar espaço em programas com restrição de memória. Se todos os seus números forem inferiores a 256, você poderá usar 8 vezes mais bytes que longs ou usar o armazenamento salvo para objetos mais complexos. O desenvolvedor de aplicativos Java padrão não precisa se preocupar com essas restrições, mas elas surgem.

Eficiência

Mesmo se ignorarmos o espaço, ainda estamos limitados pela CPU, que possui apenas instruções que operam em um número fixo de bytes (8 bytes em uma arquitetura de 64 bits). Isso significa que mesmo o fornecimento de um único longtipo de 8 bytes tornaria a implementação da linguagem significativamente mais simples do que ter um tipo de número natural ilimitado, podendo mapear operações aritméticas diretamente para uma única instrução subjacente da CPU. Se você permitir que o programador use números arbitrariamente grandes, uma única operação aritmética deverá ser mapeada para uma sequência de instruções complexas da máquina, o que atrasaria o programa. Este é o ponto (1) que você mencionou.

Tipos de ponto flutuante

Até agora, a discussão envolveu apenas números inteiros. Os tipos de ponto flutuante são uma fera complexa, com semântica extremamente sutil e casos extremos. Assim, mesmo que poderia facilmente substituir int, long, short, e bytecom um único nattipo, não está claro o que o tipo de números de ponto flutuante ainda é . Eles não são números reais, obviamente, pois números reais não podem existir em uma linguagem de programação. Também não são números bastante racionais (embora seja fácil criar um tipo racional, se desejado). Basicamente, o IEEE decidiu um meio de aproximar números reais, e todos os idiomas (e programadores) estão presos a eles desde então.

Finalmente:

Talvez o programador não queira que alguém possa usar um número maior que um determinado tamanho e isso permita que ele o limite.

Este não é um motivo válido. Em primeiro lugar, não consigo pensar em nenhuma situação em que os tipos possam codificar naturalmente os limites numéricos, para não mencionar que as chances são astronomicamente baixas de que os limites que o programador deseja impor corresponderiam exatamente aos tamanhos de qualquer um dos tipos primitivos.

Gardenhead
fonte
2
a verdadeira chave para o fato de termos flutuadores é que temos hardware dedicado para eles
jk.
também codificar limites numéricos em um tipo acontece absolutamente em idiomas de tipo dependente e, em menor grau, em outros idiomas, como enums
jk.
3
Enums não são equivalentes a números inteiros. Enums são apenas um modo de uso de tipos de soma. O fato de alguns idiomas codificarem enumerações de forma transparente como números inteiros é uma falha de idioma, não um recurso explorável.
gardenhead
1
Eu não estou familiarizado com Ada. Eu poderia restringir números inteiros a qualquer tipo, por exemplo type my_type = int (7, 2343)?
gardenhead 27/09/16
1
Sim. A sintaxe seria: type my_type is range 7..2343
Devsman 27/09/16
9

O motivo é muito simples: eficiência . De várias maneiras.

  1. Tipos de dados nativos: quanto mais próximos os tipos de dados de um idioma corresponderem aos tipos de dados subjacentes do hardware, mais eficiente o idioma será considerado. (Não no sentido de que seus programas serão necessariamente eficientes, mas no sentido de que você pode, se realmente souber o que está fazendo, escrever um código que seja tão eficiente quanto o hardware.) Os tipos de dados oferecidos por Java corresponde a bytes, palavras, palavras duplas e quadrúpedes do hardware mais popular disponível no mercado. Esse é o caminho mais eficiente a seguir.

  2. Sobrecarga injustificada em sistemas de 32 bits: se tivesse sido tomada a decisão de mapear tudo para um tamanho fixo de 64 bits, isso teria imposto uma penalidade enorme às arquiteturas de 32 bits que precisam de consideravelmente mais ciclos de clock para executar um processo de 64 bits. operação de bits que uma operação de 32 bits.

  3. Desperdício de memória: há muito hardware por aí que não é muito exigente quanto ao alinhamento de memória (as arquiteturas Intel x86 e x64 são exemplos disso), portanto uma matriz de 100 bytes nesse hardware pode ocupar apenas 100 bytes de memória. No entanto, se você não tiver mais um byte e precisar usar um longo, a mesma matriz ocupará uma ordem de magnitude a mais de memória. E matrizes de bytes são muito comuns.

  4. Calculando tamanhos de número: Sua noção de determinar dinamicamente o tamanho de um número inteiro, dependendo do tamanho do número passado, é muito simplista; não existe um ponto único para "passar" um número; o cálculo do tamanho de um número precisa ser executado em tempo de execução, em cada operação que exija um resultado de tamanho maior: toda vez que você incrementa um número, toda vez que você adiciona dois números, toda vez que você multiplica dois números etc.

  5. Operações em números de tamanhos diferentes: Posteriormente, ter números de tamanhos potencialmente diferentes flutuando na memória complicaria todas as operações: Mesmo para simplesmente comparar dois números, o tempo de execução precisaria primeiro verificar se os dois números a serem comparados são iguais. tamanho e, se não, redimensione o menor para corresponder ao tamanho do maior.

  6. Operações que requerem tamanhos específicos de operando: Certas operações bit a bit dependem do número inteiro que possui um tamanho específico. Não tendo tamanho específico pré-determinado, essas operações teriam que ser emuladas.

  7. Sobrecarga do polimorfismo: alterar o tamanho de um número em tempo de execução significa essencialmente que ele deve ser polimórfico. Isso, por sua vez, significa que não pode ser uma primitiva de tamanho fixo alocada na pilha, deve ser um objeto alocado na pilha. Isso é terrivelmente ineficiente. (Releia o item 1 acima.)

Mike Nakis
fonte
6

Para evitar repetir os pontos discutidos em outras respostas, tentarei esboçar várias perspectivas.

Do ponto de vista do design da linguagem

  • Certamente é possível projetar e implementar uma linguagem de programação e seu ambiente de execução que acomodará automaticamente os resultados de operações inteiras que não cabem na largura da máquina.
  • É a escolha do criador do idioma se tais números inteiros de largura dinâmica devem ser o tipo inteiro padrão para esse idioma.
  • No entanto, o designer de idiomas deve considerar as seguintes desvantagens:
    • A CPU terá que executar mais código, o que leva mais tempo. No entanto, é possível otimizar para o caso mais frequente em que o número inteiro se encaixa em uma única palavra de máquina. Consulte representação do ponteiro marcado .
    • O tamanho desse inteiro se torna dinâmico.
    • A leitura de um número inteiro de largura dinâmica da memória pode exigir mais de um disparo.
    • Estruturas (objetos) e matrizes que contêm números inteiros de largura dinâmica dentro de seus campos / elementos terão um tamanho total (ocupado) que também é dinâmico.

Razões históricas

Isso já é discutido no artigo da Wikipedia sobre a história do Java e também é discutido brevemente na resposta de Marco13 .

Eu apontaria que:

  • Os designers de linguagem devem fazer malabarismos entre uma mentalidade estética e uma pragmática. A mentalidade estética deseja projetar uma linguagem que não seja propensa a problemas conhecidos, como estouros de números inteiros. A mentalidade pragmática lembra ao projetista que a linguagem de programação precisa ser boa o suficiente para implementar aplicativos úteis de software e para interoperar com outras partes do software implementadas em diferentes idiomas.
  • Linguagens de programação que pretendem capturar participação de mercado de linguagens de programação antigas podem estar mais inclinadas a ser pragmáticas. Uma conseqüência possível é que eles estão mais dispostos a incorporar ou emprestar construções e estilos de programação existentes dessas linguagens mais antigas.

Razões de eficiência

Quando a eficiência importa?

  • Quando você pretende anunciar uma linguagem de programação como adequada para o desenvolvimento de aplicativos em larga escala.
  • Quando você precisa trabalhar com milhões e bilhões de itens pequenos, nos quais aumenta a eficiência.
  • Quando você precisa competir com outra linguagem de programação, sua linguagem precisa ter um desempenho decente - ela não precisa ser a melhor, mas certamente ajuda a ficar perto do melhor desempenho.

Eficiência de armazenamento (na memória ou no disco)

  • A memória do computador já foi um recurso escasso. Naquela época, o tamanho dos dados do aplicativo que podiam ser processados ​​por um computador era limitado pela quantidade de memória do computador, embora isso pudesse ser contornado com o uso de programação inteligente (que custaria mais para ser implementada).

Eficiência de execução (na CPU ou entre CPU e memória)

  • Já discutido na resposta de Gardenhead .
  • Se um programa precisar processar matrizes muito grandes de pequenos números armazenados consecutivamente, a eficiência da representação na memória afeta diretamente o desempenho da execução, porque a grande quantidade de dados faz com que a taxa de transferência entre a CPU e a memória se torne um gargalo. Nesse caso, compactar dados com mais densidade significa que uma única busca de linha de cache pode recuperar mais partes de dados.
  • No entanto, esse raciocínio não se aplica se os dados não forem armazenados ou processados ​​consecutivamente.

A necessidade de linguagens de programação para fornecer uma abstração para números inteiros pequenos, mesmo que limitados a contextos específicos

  • Essas necessidades geralmente surgem no desenvolvimento de bibliotecas de software, incluindo as bibliotecas padrão da própria linguagem. Abaixo estão vários desses casos.

Interoperabilidade

  • Freqüentemente, linguagens de programação de nível superior precisam interagir com o sistema operacional, ou partes de software (bibliotecas) escritas em outras linguagens de nível inferior. Essas linguagens de nível inferior geralmente se comunicam usando "estruturas" , que é uma especificação rígida do layout de memória de um registro que consiste em campos de tipos diferentes.
  • Por exemplo, um idioma de nível superior pode precisar especificar que uma determinada função estrangeira aceite uma charmatriz de tamanho 256. (Exemplo.)
  • Algumas abstrações usadas pelos sistemas operacionais e sistemas de arquivos requerem o uso de fluxos de bytes.
  • Algumas linguagens de programação optam por fornecer funções utilitárias (por exemplo BitConverter) para ajudar a empacotar e descompactar números inteiros estreitos em fluxos de bits e fluxos de bytes.
  • Nesses casos, os tipos inteiros mais estreitos não precisam ser do tipo primitivo incorporado ao idioma. Em vez disso, eles podem ser fornecidos como um tipo de biblioteca.

Manipulação de String

  • Existem aplicativos cujas principais finalidades de design são manipular seqüências de caracteres. Portanto, a eficiência do manuseio de strings é importante para esses tipos de aplicativos.

Manipulação de formato de arquivo

  • Muitos formatos de arquivo foram projetados com uma mentalidade semelhante ao C. Como tal, prevaleceu o uso de campos de largura estreita.

Desejabilidade, qualidade do software e responsabilidade do programador

  • Para muitos tipos de aplicativos, o alargamento automático de números inteiros não é realmente um recurso desejável. Nem a saturação nem o contorno (módulo).
  • Muitos tipos de aplicativos se beneficiarão da especificação explícita do programador dos maiores valores permitidos em vários pontos críticos do software, como no nível da API.

Considere o seguinte cenário.

  • Uma API de software aceita uma solicitação JSON. A solicitação contém uma matriz de solicitações filho. A solicitação JSON inteira pode ser compactada com o algoritmo Deflate.
  • Um usuário mal-intencionado cria uma solicitação JSON contendo um bilhão de solicitações filho. Todas as solicitações filho são idênticas; o usuário mal-intencionado pretende que o sistema grave alguns ciclos da CPU fazendo um trabalho inútil. Devido à compactação, essas solicitações filho idênticas são compactadas para um tamanho total muito pequeno.
  • É óbvio que um limite predefinido no tamanho compactado dos dados não é suficiente. Em vez disso, a API precisa impor um limite predefinido ao número de solicitações filho que podem estar contidas nela e / ou um limite predefinido no tamanho deflacionado dos dados.

Freqüentemente, o software que pode escalar com segurança muitas ordens de magnitude deve ser projetado para esse fim, com crescente complexidade. Ele não vem automaticamente, mesmo se o problema de excesso de número inteiro for eliminado. Isso chega a um círculo completo, respondendo à perspectiva do design da linguagem: geralmente, o software que se recusa a executar um trabalho quando ocorre um estouro indesejado de número inteiro (lançando um erro ou exceção) é melhor do que o software que cumpre automaticamente as operações astronomicamente grandes.

Isso significa a perspectiva do OP,

Por que é necessário que uma pessoa defina quantos bytes devem ser usados ​​para um valor primitivo?

não está correto. O programador deve ter permissão, e algumas vezes necessário, para especificar a magnitude máxima que um valor inteiro pode ter, em partes críticas do software. Como aponta a resposta de Gardenhead , os limites naturais impostos pelos tipos primitivos não são úteis para esse fim; a linguagem deve fornecer maneiras para os programadores declararem magnitudes e aplicarem esses limites.

rwong
fonte
2

Tudo vem do hardware.

Um byte é a menor unidade de memória endereçável na maioria dos hardwares.

Todo tipo que você mencionou é criado a partir de vários bytes.

Um byte é de 8 bits. Com isso, você pode expressar 8 booleanos, mas não pode procurar apenas um de cada vez. Você endereça 1, está endereçando todos os 8.

E costumava ser tão simples, mas depois passamos de um barramento de 8 bits para um de 16, 32 e agora de 64 bits.

O que significa que, embora ainda possamos endereçar no nível de bytes, não podemos mais recuperar um único byte da memória sem obter os bytes vizinhos.

Diante desse hardware, os designers de idiomas escolheram para nos permitir escolher tipos que nos permitissem escolher tipos que se encaixassem no hardware.

Você pode afirmar que esse detalhe pode e deve ser abstraído, especialmente em um idioma que visa executar em qualquer hardware. Isso ocultaria problemas de desempenho, mas você pode estar certo. Simplesmente não aconteceu dessa maneira.

Java realmente tenta fazer isso. Os bytes são promovidos automaticamente para Ints. Um fato que o deixará maluco na primeira vez em que tentar fazer algum trabalho sério de mudança de bits.

Então, por que não funcionou bem?

O grande ponto de venda de Java na época em que você podia se sentar com um bom algoritmo C conhecido, digitá-lo em Java e, com pequenos ajustes, ele funcionaria. E C está muito próximo do hardware.

Manter esse tamanho ativo e abstrato fora dos tipos integrais simplesmente não funcionava juntos.

Então eles poderiam ter. Eles simplesmente não.

Talvez o programador não queira que alguém possa usar um número maior que um determinado tamanho e isso permita que ele o limite.

Este é um pensamento válido. Existem métodos para fazer isso. A função de grampo para um. Um idioma pode chegar ao ponto de estabelecer limites arbitrários em seus tipos. E quando esses limites são conhecidos em tempo de compilação, isso permitiria otimizações na maneira como esses números são armazenados.

Java simplesmente não é essa linguagem.

candied_orange
fonte
" Uma linguagem pode chegar a estabelecer limites arbitrários em seus tipos ". E, de fato, Pascal tem uma forma disso com tipos de subintervalos.
Peter Taylor
1

Provavelmente, uma importante razão pela qual esses tipos existem em Java é simples e, infelizmente, não é técnica:

C e C ++ também tinham esses tipos!

Embora seja difícil fornecer uma prova de que esse é o motivo, há pelo menos algumas evidências fortes: A Oak Language Specification (Versão 0.2) contém a seguinte passagem:

3.1 Tipos Inteiros

Os números inteiros na linguagem Oak são semelhantes aos de C e C ++, com duas exceções: todos os tipos de números inteiros são independentes de máquina e algumas das definições tradicionais foram alteradas para refletir as mudanças no mundo desde que C foi introduzido. Os quatro tipos inteiros têm larguras de 8, 16, 32 e 64 bits e são assinados, a menos que prefixados pelo unsignedmodificador.

Portanto, a pergunta pode se resumir a:

Por que curto, int e muito inventado em C?

Não tenho certeza se a resposta à pergunta da carta é satisfatória no contexto da pergunta que foi feita aqui. Mas, em combinação com as outras respostas aqui, pode ficar claro que pode ser benéfico ter esses tipos (independentemente de sua existência em Java ser apenas um legado do C / C ++).

As razões mais importantes em que consigo pensar são

  • Um byte é a menor unidade de memória endereçável (como CandiedOrange já mencionado). A byteé o elemento básico dos dados, que pode ser lido de um arquivo ou pela rede. Alguma representação explícita disso deve existir (e existe na maioria dos idiomas, mesmo quando às vezes aparece disfarçada).

  • É verdade que, na prática, faria sentido representar todos os campos e variáveis ​​locais usando um único tipo e chamar esse tipo int. Há uma pergunta relacionada sobre isso no stackoverflow: Por que a API Java usa int em vez de curto ou byte? . Como mencionei na minha resposta, uma justificativa para ter os tipos menores ( bytee short) é que você pode criar matrizes desses tipos: Java possui uma representação de matrizes que ainda estão "próximas do hardware". Em contraste com outras linguagens (e em contraste com matrizes de objetos, como uma Integer[n]matriz), uma int[n]matriz não é uma coleção de referências onde os valores estão espalhados pelo heap. Em vez disso, ele vaina prática, seja um bloco consecutivo de n*4bytes - um pedaço de memória com tamanho e layout de dados conhecidos. Quando você tem a opção de armazenar 1000 bytes em uma coleção de objetos de valor inteiro de tamanho arbitrário ou em um byte[1000](que ocupa 1000 bytes), o último pode realmente economizar memória. (Algumas outras vantagens disso podem ser mais sutis e só se tornam óbvias ao fazer a interface do Java com bibliotecas nativas)


Em relação aos pontos que você perguntou especificamente sobre:

O tamanho não podia ser determinado dinamicamente, dependendo do tamanho do número passado?

Definir dinamicamente o tamanho dos dados significaria que também seria necessário alterar dinamicamente. Isso pode causar problemas de desempenho?

Provavelmente seria possível definir dinamicamente o tamanho das variáveis, se alguém considerasse projetar uma linguagem de programação completamente nova do zero. Não sou especialista na construção de compiladores, mas acho que seria difícil gerenciar coleções de tipos que mudam dinamicamente de maneira sensata - principalmente quando você tem uma linguagem fortemente tipada. Portanto, provavelmente se resumiria a todos os números armazenados em um "tipo de dados de número de precisão arbitrário genérico", o que certamente teria impactos no desempenho. É claro que existem linguagens de programação fortemente tipadas e / ou que oferecem tipos de números de tamanho arbitrário, mas não acho que exista uma linguagem de programação real de propósito geral que tenha sido assim.


Notas laterais:

  • Você pode ter se perguntado sobre o unsignedmodificador mencionado nas especificações do Oak. De fato, ele também contém uma observação: " unsignedainda não foi implementado; talvez nunca o seja". . E eles estavam certos.

  • Além de se perguntar por que o C / C ++ tinha esses tipos inteiros diferentes, você pode se perguntar por que eles os atrapalharam tão horrivelmente que você nunca sabe quantos bits um inttem. As justificativas para isso geralmente estão relacionadas ao desempenho e podem ser consultadas em outros lugares.

Marco13
fonte
0

Certamente mostra que você ainda não foi ensinado sobre desempenho e arquiteturas.

  • Primeiro, nem todo processador pode lidar com os grandes tipos; portanto, você precisa conhecer as limitações e trabalhar com isso.
  • Segundo, tipos menores significam mais desempenho ao realizar operações.
  • Além disso, o tamanho é importante, se você precisar armazenar dados em um arquivo ou banco de dados, o tamanho afetará o desempenho e o tamanho final de todos os dados, por exemplo, digamos que você tenha uma tabela com 15 colunas e acabe com várias milhões de registros. A diferença entre escolher um tamanho pequeno conforme necessário para cada coluna ou escolher apenas o maior, será a diferença de possíveis Gigs de dados e tempo no desempenho das operações.
  • Além disso, aplica-se a cálculos complexos, nos quais o tamanho dos dados processados ​​terá grande impacto, como nos jogos, por exemplo.

Ignorando a importância do tamanho dos dados sempre atinge o desempenho, você deve usar quantos recursos forem necessários, mas não mais, sempre!

Essa é a diferença entre um programa ou sistema que faz coisas realmente simples e é incrivelmente ineficiente, exigindo muitos recursos e tornando o uso desse sistema muito caro; ou um sistema que faz muito, mas roda mais rápido que os outros e é muito barato de executar.

Nestor Mata Cuthbert
fonte
0

Existem algumas boas razões

(1) enquanto o armazenamento de uma variável de um byte com um comprimento é insignificante, o armazenamento de milhões em uma matriz é muito significativo.

(2) a aritmética "hardware nativo" baseada em tamanhos inteiros específicos pode ser muito mais eficiente e, para alguns algoritmos em algumas plataformas, isso pode ser importante.

ddyer
fonte