Cultura de Grokking Java - por que as coisas são tão pesadas? Para que ele otimiza? [fechadas]

288

Eu costumava codificar bastante em Python. Agora, por motivos de trabalho, eu codigo em Java. Os projetos que faço são bastante pequenos e, possivelmente, o Python funcionaria melhor, mas há motivos válidos de não engenharia para usar Java (não posso entrar em detalhes).

Sintaxe Java não é problema; é apenas outra língua. Mas, além da sintaxe, o Java possui uma cultura, um conjunto de métodos de desenvolvimento e práticas consideradas "corretas". E, por enquanto, falho completamente em "grok" essa cultura. Então, eu realmente aprecio explicações ou indicações na direção certa.

Um exemplo completo mínimo está disponível em uma pergunta de Estouro de pilha que eu iniciei: https://stackoverflow.com/questions/43619566/returning-a-result-with-several-values-the-java-way/43620339

Eu tenho uma tarefa - analisar (de uma única seqüência de caracteres) e manipular um conjunto de três valores. No Python, é uma linha única (tupla), em Pascal ou C, um registro / estrutura de 5 linhas.

De acordo com as respostas, o equivalente a uma struct está disponível na sintaxe Java e o triplo está disponível em uma biblioteca Apache amplamente usada - mas a maneira "correta" de fazer isso é na verdade criando uma classe separada para o valor, concluída com getters e setters. Alguém foi muito gentil em fornecer um exemplo completo. Eram 47 linhas de código (bem, algumas dessas linhas eram espaços em branco).

Entendo que uma enorme comunidade de desenvolvimento provavelmente não esteja "errada". Portanto, este é um problema com o meu entendimento.

As práticas do Python otimizam a legibilidade (que, nessa filosofia, leva à capacidade de manutenção) e depois a velocidade de desenvolvimento. As práticas de C otimizam para o uso de recursos. Para que as práticas Java são otimizadas? Meu melhor palpite é escalabilidade (tudo deve estar em um estado pronto para um projeto com milhões de LOC), mas é um palpite muito fraco.

Mikhail Ramendik
fonte
1
não responderá aos aspectos técnicos da pergunta, mas ainda no contexto: softwareengineering.stackexchange.com/a/63145/13899
Newtopian
74
Você pode apreciar o ensaio de Steve Yegge Execution in the Kingdom of Nouns
Coronel Panic
3
Aqui está o que eu acho que é a resposta certa. Você não está aprendendo Java porque pensa em programar como um administrador de sistemas, não como programador. Para você, o software é algo que você usa para realizar uma tarefa específica da maneira mais conveniente. Escrevo código Java há 20 anos, e alguns dos projetos em que trabalhei levaram uma equipe de 20, 2 anos para serem alcançados. Java não substitui o Python nem vice-versa. Ambos trabalham, mas são totalmente diferentes.
Richard
1
A razão pela qual o Java é o padrão de fato é porque ele está simplesmente certo. Foi criado logo pela primeira vez por pessoas sérias que tinham barbas antes da moda. Se você está acostumado a usar scripts de shell para manipular arquivos, o Java parece intoleravelmente inchado. Mas se você deseja criar um cluster de servidor redundante duplo capaz de atender 50 milhões de pessoas por dia, apoiado por cache redis em cluster, 3 sistemas de cobrança e um cluster de banco de dados Oracle de 20 milhões de libras. Você não usará python, mesmo acessando o banco de dados em Python tem 25 linhas a menos de código.
Richard

Respostas:

237

A linguagem Java

Acredito que todas essas respostas estão erradas ao tentar atribuir a intenção à maneira como o Java funciona. A verbosidade do Java não deriva de ser orientada a objetos, pois Python e muitas outras linguagens ainda têm sintaxe de terser. A verbosidade do Java também não provém do suporte a modificadores de acesso. Em vez disso, é simplesmente como o Java foi projetado e evoluiu.

O Java foi originalmente criado como um C ligeiramente melhorado com OO. Como tal, o Java possui uma sintaxe dos anos 70. Além disso, o Java é muito conservador em adicionar recursos para manter a compatibilidade com versões anteriores e permitir que ele resista ao teste do tempo. Se o Java tivesse adicionado recursos modernos, como literais XML, em 2005, quando o XML era a moda, a linguagem teria sido inchada com recursos fantasmas com os quais ninguém se importa e que limitam sua evolução 10 anos depois. Portanto, o Java simplesmente não possui muita sintaxe moderna para expressar conceitos de maneira concisa.

No entanto, não há nada fundamental que impeça o Java de adotar essa sintaxe. Por exemplo, o Java 8 adicionou lambdas e referências a métodos, reduzindo bastante a verbosidade em muitas situações. Da mesma forma, o Java poderia adicionar suporte para declarações compactas de tipos de dados, como as classes de caso da Scala. Mas o Java simplesmente não o fez. Observe que os tipos de valores personalizados estão no horizonte e esse recurso pode introduzir uma nova sintaxe para declará-los. Suponho que vamos ver.


A cultura Java

A história do desenvolvimento Java corporativo nos levou em grande parte à cultura que vemos hoje. No final dos anos 90 / início dos anos 00, o Java se tornou uma linguagem extremamente popular para aplicativos de negócios do lado do servidor. Naquela época, esses aplicativos eram amplamente escritos ad-hoc e incorporavam muitas preocupações complexas, como APIs HTTP, bancos de dados e processamento de feeds XML.

Nos anos 2000, ficou claro que muitos desses aplicativos tinham muito em comum e as estruturas para gerenciar essas preocupações, como o Hibernate ORM, o analisador XML do Xerces, JSPs e a API do servlet e EJB, se tornaram populares. No entanto, embora essas estruturas reduzissem o esforço de trabalhar no domínio específico que elas definiam para automatizar, elas exigiam configuração e coordenação. Na época, por qualquer motivo, era popular escrever estruturas para atender ao caso de uso mais complexo e, portanto, essas bibliotecas eram complicadas de configurar e integrar. E, com o tempo, eles se tornaram cada vez mais complexos à medida que acumulavam recursos. O desenvolvimento corporativo de Java tornou-se cada vez mais cada vez mais vinculado a bibliotecas de terceiros e menos sobre algoritmos de escrita.

Eventualmente, a configuração e o gerenciamento tediosos das ferramentas corporativas tornaram-se dolorosos o suficiente para que as estruturas, principalmente a Spring, surgissem para gerenciar o gerenciamento. Você poderia colocar toda a sua configuração em um só lugar, continuou a teoria, e a ferramenta de configuração configuraria as peças e as uniria. Infelizmente, essas "estruturas de estrutura" adicionaram mais abstração e complexidade em cima de toda a bola de cera.

Nos últimos anos, mais bibliotecas leves cresceram em popularidade. No entanto, toda uma geração de programadores Java atingiu a maioridade durante o crescimento de estruturas corporativas pesadas. Seus modelos, aqueles que desenvolvem as estruturas, escreveram fábricas de fábrica e carregadores de bean de configuração de proxy. Eles tiveram que configurar e integrar essas monstruosidades no dia-a-dia. E, como resultado, a cultura da comunidade como um todo seguiu o exemplo dessas estruturas e tendia a um excesso de engenharia.

Segredo de Salomão
fonte
15
O engraçado é que outras línguas parecem estar pegando alguns "java-ismos". Os recursos do PHP OO são fortemente inspirados em Java e hoje em dia o JavaScript é frequentemente criticado por sua enorme quantidade de estruturas e pela dificuldade de reunir todas as dependências e iniciar um novo aplicativo.
Marcus
14
@ Marcus talvez porque algumas pessoas finalmente aprenderam a coisa "não reinventar a roda"? Dependências são o custo de não reinventar a roda, afinal de contas
Walfrat
9
"Terse" geralmente se traduz em arcadas únicas, "inteligentes" e de código-golfe-ish.
Tulains Córdova
7
Resposta pensativa e bem escrita. Contexto histórico. Relativamente sem opinião. Bem feito.
Cheezmeister 29/04/19
10
@ TulainsCórdova Concordo que devemos "evitar ... truques inteligentes como a praga" (Donald Knuth), mas isso não significa escrever um forloop quando um mapseria mais apropriado (e legível). Há um meio feliz.
Brian McCutchon
73

Acredito que tenho uma resposta para um dos pontos que você levanta e que não foram levantados por outros.

Java otimiza para detecção de erros de programador em tempo de compilação, nunca fazendo suposições

Em geral, Java tende a inferir apenas fatos sobre o código-fonte depois que o programador já expressou explicitamente sua intenção. O compilador Java nunca faz suposições sobre o código e usará apenas inferência para reduzir o código redundante.

A razão por trás dessa filosofia é que o programador é apenas humano. O que escrevemos nem sempre é o que realmente pretendemos que o programa faça. A linguagem Java tenta mitigar alguns desses problemas, forçando o desenvolvedor a sempre declarar explicitamente seus tipos. Essa é apenas uma maneira de verificar se o código que foi escrito realmente faz o que foi planejado.

Algumas outras linguagens aprimoram ainda mais essa lógica, verificando pré-condições, pós-condições e invariantes (embora não tenha certeza de que o façam em tempo de compilação). Essas são formas ainda mais extremas para o programador fazer com que o compilador verifique novamente seu próprio trabalho.

No seu caso, isso significa que, para que o compilador garanta que você está realmente retornando os tipos que pensa estar retornando, é necessário fornecer essas informações ao compilador.

Em Java, existem duas maneiras de fazer isso:

  1. Use Triplet<A, B, C>como tipo de retorno (que realmente deve ser em java.util, e eu realmente não posso explicar por que não é. Especialmente desde JDK8 introduzir Function, BiFunction, Consumer, BiConsumer, etc ... Parece apenas que Paire Triplet, pelo menos, faria sentido. Mas estou divagando )

  2. Crie seu próprio tipo de valor para esse fim, em que cada campo é nomeado e digitado corretamente.

Nos dois casos, o compilador pode garantir que sua função retorne os tipos declarados e que o chamador perceba qual é o tipo de cada campo retornado e os use adequadamente.

Alguns idiomas fornecem verificação estática de tipos E inferência de tipos ao mesmo tempo, mas isso deixa a porta aberta para uma classe sutil de problemas de incompatibilidade de tipos. Onde o desenvolvedor pretendeu retornar um valor de um determinado tipo, mas na verdade retorna outro e o compilador AINDA aceita o código, porque acontece que, por co-incidência, a função e o chamador usam apenas métodos que podem ser aplicados tanto ao pretendido e os tipos reais.

Considere algo como isto em Texto datilografado (ou tipo de fluxo) em que a inferência de tipo é usada em vez da digitação explícita.

function parseDurationInMillis(csvLine){
    // here the developer intends to return a Number, 
    // but actually it's a String
    return csv.firstField();
}

// Compiler infers that parseDurationInMillis is String, so it does
// string concatenation and infers that plusTwoSeconds is String
// Developer actually intended Number
var plusTwoSeconds = 2000 + parseDurationInMillis(csvLine);

Esse é um caso trivial bobo, é claro, mas pode ser muito mais sutil e levar a problemas difíceis de depurar, porque o código parece correto. Esse tipo de problema é totalmente evitado em Java, e é para isso que toda a linguagem é projetada.


Observe que, seguindo os princípios orientados a objetos e a modelagem orientada a domínio, a duração da análise de caso na pergunta vinculada também pode ser retornada como um java.time.Durationobjeto, o que seria muito mais explícito do que os dois casos acima.

LordOfThePigs
fonte
38
Afirmar que o Java otimiza a correção do código pode ser um ponto válido, mas parece-me (programando muitas linguagens, recentemente um pouco de java) que a linguagem falha ou é extremamente ineficiente ao fazê-lo. É verdade que o Java é antigo e muitas coisas não existiam naquela época, mas existem alternativas modernas que fornecem uma verificação de correção sem dúvida melhor, como Haskell (e ouso dizer? Rust or Go). Toda a dificuldade de Java é desnecessária para esse objetivo. - E, além disso, este não em tudo explicar a cultura, mas Solomonoff revelou que a cultura é BS qualquer maneira.
tomsmeding
8
Esse exemplo não demonstra que a inferência de tipo é o problema, apenas que a decisão do JavaScript de permitir conversões implícitas em uma linguagem dinâmica é estúpida. Qualquer linguagem dinâmica sã ou linguagem divulgada estaticamente com inferências de tipo não permitiria isso. Como um exemplo para os dois tipos: python lançaria um tempo de execução, exceto Haskell não compilaria.
Voo 26/04
39
@tomsmeding Vale a pena notar que esse argumento é especialmente tolo porque Haskell existe há 5 anos a mais que Java. O Java pode ter um sistema de tipos, mas nem tinha verificação de correção de última geração quando foi lançado.
Alexis King
21
“Java otimiza para detecção de erros em tempo de compilação” - Não. Ele fornece , mas, como outros observaram, certamente não é otimizado para isso em nenhum sentido da palavra. Além disso, este argumento é completamente alheios à pergunta de OP, porque outras línguas que não realmente otimizar para detecção de erros em tempo de compilação têm muito mais leve sintaxe para cenários semelhantes. A verbosidade exagerada do Java não tem exatamente nada a ver com a verificação em tempo de compilação.
Konrad Rudolph
19
@KonradRudolph Sim, esta resposta é completamente sem sentido, embora eu suponha que seja emocionalmente atraente. Não sei por que tem tantos votos. Haskell (ou talvez ainda mais significativamente, Idris) otimiza muito mais a correção do que o Java e possui tuplas com uma sintaxe leve. Além disso, definir um tipo de dados é uma linha e você obtém tudo o que a versão Java obteria. Essa resposta é uma desculpa para um design de linguagem ruim e verbosidade desnecessária, mas soa bem se você gosta de Java e não está familiarizado com outras linguagens.
Alexis King
50

Java e Python são as duas linguagens que mais uso, mas estou vindo de outra direção. Ou seja, eu estava no mundo Java antes de começar a usar o Python, para poder ajudar. Eu acho que a resposta para a questão maior de "por que as coisas são tão pesadas" se resume a duas coisas:

  • Os custos em torno do desenvolvimento entre os dois são como o ar em um longo balão de animal. Você pode apertar uma parte do balão e outra parte incha. Python tende a espremer a parte inicial. Java aperta a parte posterior.
  • O Java ainda carece de alguns recursos que removeriam parte desse peso. O Java 8 causou um grande estrago nisso, mas a cultura não digeriu completamente as mudanças. Java poderia usar mais algumas coisas, como yield.

Java 'otimiza' para software de alto valor que será mantido por muitos anos por grandes equipes de pessoas. Eu tive a experiência de escrever coisas em Python e analisá-las um ano depois e ficar perplexo com meu próprio código. Em Java, posso ver pequenos trechos do código de outras pessoas e saber instantaneamente o que ele faz. No Python, você realmente não pode fazer isso. Não é que um seja melhor como você parece perceber, eles apenas têm custos diferentes.

No caso específico que você mencionou, não há tuplas. A solução fácil é criar uma classe com valores públicos. Quando o Java foi lançado, as pessoas faziam isso regularmente. O primeiro problema ao fazer isso é que é uma dor de cabeça de manutenção. Se você precisar adicionar alguma lógica ou segurança de encadeamento ou quiser usar o polimorfismo, precisará, no mínimo, tocar em todas as classes que interagem com o objeto 'tupla-esque'. No Python, existem soluções para isso, como __getattr__etc., por isso não é tão terrível.

Existem alguns maus hábitos (IMO) em torno disso. Nesse caso, se você deseja uma tupla, questiono por que você a tornaria um objeto mutável. Você só precisa de getters (em uma nota lateral, eu odeio a convenção get / set, mas é o que é.) Eu acho que uma classe simples (mutável ou não) pode ser útil em um contexto privado ou pacote-privado em Java . Ou seja, ao limitar as referências no projeto à classe, você pode refatorar mais tarde, conforme necessário, sem alterar a interface pública da classe. Aqui está um exemplo de como você pode criar um objeto simples e imutável:

public class Blah 
{
  public static Blah blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    return new Blah(number, isInSeconds, lessThanOneMillis);
  }

  private final long number;
  private final boolean isInSeconds;
  private final boolean lessThanOneMillis;

  public Blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    this.number = number;
    this.isInSeconds = isInSeconds;
    this.lessThanOneMillis = lessThanOneMillis;
  }

  public long getNumber()
  {
    return number;
  }

  public boolean isInSeconds()
  {
    return isInSeconds;
  }

  public boolean isLessThanOneMillis()
  {
    return lessThanOneMillis;
  }
}

Esse é um tipo de padrão que eu uso. Se você não estiver usando um IDE, você deve começar. Ele irá gerar os getters (e setters, se você precisar deles) para você, então isso não é tão doloroso.

Eu me sentiria negligente se não indicasse que já existe um tipo que parece atender a maioria das suas necessidades aqui . Deixando isso de lado, a abordagem que você está usando não é adequada para Java, pois joga com suas fraquezas, não com seus pontos fortes. Aqui está uma melhoria simples:

public class Blah 
{
  public static Blah fromSeconds(long number)
  {
    return new Blah(number * 1000_000);
  }

  public static Blah fromMills(long number)
  {
    return new Blah(number * 1000);
  }

  public static Blah fromNanos(long number)
  {
    return new Blah(number);
  }

  private final long nanos;

  private Blah(long nanos)
  {
    this.nanos = nanos;
  }

  public long getNanos()
  {
    return nanos;
  }

  public long getMillis()
  {
    return getNanos() / 1000; // or round, whatever your logic is
  }

  public long getSeconds()
  {
    return getMillis() / 1000; // or round, whatever your logic is
  }

  /* I don't really know what this is about but I hope you get the idea */
  public boolean isLessThanOneMillis()
  {
    return getMillis() < 1;
  }
}
JimmyJames
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
maple_shaft
2
Posso sugerir que você também olhar para Scala no que diz respeito a recursos Java "mais modernos" ...
MikeW
1
@ MikeW Na verdade, eu comecei com o Scala nos primeiros lançamentos e fiquei ativo nos fóruns do scala por um tempo. Eu acho que é uma grande conquista no design de idiomas, mas cheguei à conclusão de que não era exatamente o que eu estava procurando e que eu era um pegador quadrado nessa comunidade. Eu deveria olhar novamente, porque provavelmente mudou significativamente desde então.
precisa saber é o seguinte
Esta resposta não aborda o aspecto aproximado do valor levantado pelo OP.
ErikE
@ErikE As palavras "valor aproximado" não aparecem no texto da pergunta. Se você puder me indicar a parte específica da pergunta que não atendi, posso tentar fazê-lo.
JimmyJames
41

Antes de você ficar muito bravo com Java, leia minha resposta em seu outro post .

Uma de suas reclamações é a necessidade de criar uma classe apenas para retornar algum conjunto de valores como resposta. Esta é uma preocupação válida que eu acho que mostra que suas intuições de programação estão no caminho certo! No entanto, acho que outras respostas estão perdendo a marca, mantendo-se com o antipadrão de obsessão primitivo com o qual você se comprometeu. E o Java não tem a mesma facilidade de trabalhar com várias primitivas que o Python, onde você pode retornar vários valores nativamente e atribuí-los a várias variáveis ​​facilmente.

Mas quando você começa a pensar sobre o que um ApproximateDurationtipo faz por você, percebe que o escopo não é tão restrito a "apenas uma classe aparentemente desnecessária para retornar três valores". O conceito representado por essa classe é na verdade um dos conceitos principais de negócios do domínio - a necessidade de poder representar os tempos de maneira aproximada e compará-los. Isso precisa fazer parte da linguagem onipresente do núcleo do seu aplicativo, com um bom suporte a objetos e domínios, para que possa ser testado, modular, reutilizável e útil.

Seu código que soma durações aproximadas (ou durações com uma margem de erro, no entanto você representa isso) é totalmente processual ou existe algum objeto para ele? Eu proporia que um bom design em torno da soma de durações aproximadas determinaria fazê-lo fora de qualquer código consumidor, dentro de uma classe que possa ser testada. Eu acho que usar esse tipo de objeto de domínio terá um efeito cascata positivo em seu código que ajuda a desviar você das etapas de procedimento linha a linha para realizar uma única tarefa de alto nível (embora com muitas responsabilidades), em direção a classes de responsabilidade única que estão livres de conflitos de preocupações diferentes.

Por exemplo, digamos que você aprenda mais sobre qual precisão ou escala é realmente necessária para que sua soma e comparação de duração funcionem corretamente e você descobre que precisa de um sinalizador intermediário para indicar "erro de aproximadamente 32 milissegundos" (próximo ao quadrado raiz de 1000, portanto, logaritmicamente na metade entre 1 e 1000). Se você se vinculou ao código que usa primitivas para representar isso, precisará encontrar todos os lugares no código em que está is_in_seconds,is_under_1mse alterá-lo parais_in_seconds,is_about_32_ms,is_under_1ms. Tudo teria que mudar por todo o lado! Fazer uma classe cuja responsabilidade é registrar a margem de erro para que possa ser consumida em outro lugar libera seus consumidores de saberem detalhes de quais margens de erro são importantes ou qualquer coisa sobre como elas se combinam e permite que eles especifiquem apenas a margem de erro relevante. no momento. (Ou seja, nenhum código de consumo cuja margem de erro esteja correta é forçado a mudar quando você adiciona uma nova margem de nível de erro na classe, pois todas as margens de erro antigas ainda são válidas).

Declaração de encerramento

A reclamação sobre o peso do Java parece desaparecer à medida que você se aproxima dos princípios do SOLID e GRASP e da engenharia de software mais avançada.

Termo aditivo

Acrescentarei de forma totalmente gratuita e injusta que as propriedades automáticas do C # e a capacidade de atribuir propriedades get-get em construtores ajudam a limpar ainda mais o código um tanto bagunçado que "o modo Java" exigirá (com campos de apoio privados explícitos e funções getter / setter) :

// Warning: C# code!
public sealed class ApproximateDuration {
   public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
      LowMilliseconds = lowMilliseconds;
      HighMilliseconds = highMilliseconds;
   }
   public int LowMilliseconds { get; }
   public int HighMilliseconds { get; }
}

Aqui está uma implementação Java do acima:

public final class ApproximateDuration {
  private final int lowMilliseconds;
  private final int highMilliseconds;

  public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
    this.lowMilliseconds = lowMilliseconds;
    this.highMilliseconds = highMilliseconds;
  }

  public int getLowMilliseconds() {
    return lowMilliseconds;
  }

  public int getHighMilliseconds() {
    return highMilliseconds;
  }
}

Agora isso é bastante limpo. Observe o uso muito importante e intencional da imutabilidade - isso parece crucial para esse tipo específico de classe com valor agregado.

Por outro lado, essa classe também é uma candidata decente por ser um structtipo de valor. Alguns testes mostram se a mudança para uma estrutura tem um benefício de desempenho em tempo de execução (poderia).

ErikE
fonte
9
Eu acho que esta é a minha resposta favorita. Descobri que, quando promovo uma classe de detentor descartável como essa para algo adulto, ela se torna o lar de todas as responsabilidades relacionadas e zing! tudo fica mais limpo! E eu costumo aprender algo interessante sobre o espaço do problema ao mesmo tempo. Obviamente, há uma habilidade em evitar o excesso de engenharia ... mas Gosling não era estúpido quando deixou de fora a sintaxe da tupla, o mesmo que com os GOTOs. Sua declaração final é excelente. Obrigado!
precisa saber é o seguinte
2
Se você gosta desta resposta, leia sobre Design Orientado a Domínio. Há muito a dizer sobre esse tópico de representar conceitos de domínio no código.
precisa saber é
@neontapir Sim, obrigado! Eu sabia que havia um nome para isso! :-) Embora eu deva dizer que prefiro quando um conceito de domínio aparece organicamente a partir de um pouco inspirado de refatoração (assim!) ... é um pouco como quando você resolve o seu problema de gravidade do século 19 ao descobrir Netuno. .
SusanW
@SusanW Eu concordo. A refatoração para remover a obsessão primitiva pode ser uma excelente maneira de descobrir conceitos de domínio!
precisa saber é
Eu adicionei uma versão Java do exemplo, conforme discutido. Eu não gosto de todo o açúcar sintático mágico em C #, se houver algo faltando, me avise.
JimmyJames
24

Tanto o Python quanto o Java são otimizados para manutenção de acordo com a filosofia de seus designers, mas eles têm idéias muito diferentes sobre como conseguir isso.

Python é uma linguagem de vários paradigmas que otimiza a clareza e a simplicidade do código (fácil de ler e escrever).

Java é (tradicionalmente) uma linguagem OO baseada em classe de paradigma único que otimiza para explicitação e consistência - mesmo à custa de um código mais detalhado.

Uma tupla Python é uma estrutura de dados com um número fixo de campos. A mesma funcionalidade pode ser alcançada por uma classe regular com campos explicitamente declarados. No Python, é natural fornecer tuplas como alternativa às classes, pois isso permite simplificar bastante o código, principalmente devido ao suporte de sintaxe interno para tuplas.

Mas isso realmente não corresponde à cultura Java para fornecer esses atalhos, pois você já pode usar classes declaradas explícitas. Não é necessário introduzir um tipo diferente de estrutura de dados apenas para salvar algumas linhas de código e evitar algumas declarações.

Java prefere um único conceito (classes) aplicado consistentemente com um mínimo de açúcar sintático para casos especiais, enquanto o Python fornece várias ferramentas e muito açúcar sintático para permitir que você escolha o mais conveniente para qualquer finalidade específica.

JacquesB
fonte
+1, mas como isso se encaixa com linguagens estaticamente tipadas com classes, como Scala, que, no entanto, considerou a necessidade de introduzir tuplas? Eu acho que as tuplas são a solução mais adequada em alguns casos, mesmo para idiomas com aulas.
26517 Andres F.
@AndresF .: O que você quer dizer com malha? Scala é uma linguagem distinta de Java e possui diferentes princípios e expressões de design.
precisa saber é o seguinte
Eu quis dizer isso em resposta ao seu quinto parágrafo, que começa com "mas isso realmente não corresponde à cultura Java [...]". De fato, concordo que a verbosidade faz parte da cultura de Java, mas a falta de tuplas não pode ser porque "já usam classes declaradas explicitamente" - afinal, Scala também tem classes explícitas (e apresenta as classes de caso mais concisas ), mas também permite tuplas! No final, acho que o Java provavelmente não introduziu tuplas não apenas porque as classes podem obter o mesmo (com mais desorganização), mas também porque sua sintaxe deve ser familiar aos programadores C e C não possui tuplas.
26517 Andres F.
@AndresF .: Scala não tem aversão a várias maneiras de fazer as coisas, ele combina recursos do paradigma funcional e do OO clássico. É mais próximo da filosofia Python dessa maneira.
precisa saber é o seguinte
@AndresF .: Sim, o Java começou com uma sintaxe próxima ao C / C ++, mas simplificou bastante. O C # começou como uma cópia quase exata do Java, mas adicionou muitos recursos ao longo dos anos, incluindo as tuplas. Portanto, é realmente uma diferença na filosofia de design de linguagem. (As versões posteriores do Java parecem ser menos dogmáticas. A função de ordem superior foi inicialmente rejeitada porque você poderia fazer o mesmo que classes, mas agora elas foram introduzidas. Então, talvez estejamos vendo uma mudança na filosofia.)
JacquesB
16

Não procure práticas; geralmente é uma péssima idéia, como dito em Best Practices BAD, padrões BOM? . Sei que você não está pedindo melhores práticas, mas ainda acho que você encontrará alguns elementos relevantes.

Procurar uma solução para o seu problema é melhor que uma prática e seu problema não é uma tupla para retornar três valores em Java rapidamente:

  • Existem matrizes
  • Você pode retornar uma matriz como uma lista em uma linha: Arrays.asList (...)
  • Se você deseja manter o lado do objeto com menos clichê possível (e sem lombok):

class MyTuple {
    public final long value_in_milliseconds;
    public final boolean is_in_seconds;
    public final boolean is_under_1ms;
    public MyTuple(long value_in_milliseconds,....){
        ...
    }
 }

Aqui você tem um objeto imutável que contém apenas seus dados e público, para que não haja necessidade de getters. Observe que, no entanto, se você usar algumas ferramentas de serialização ou camada de persistência como um ORM, elas geralmente usam getter / setter (e podem aceitar um parâmetro para usar campos em vez de getter / setter). E é por isso que essas práticas são muito usadas. Portanto, se você quiser saber sobre práticas, é melhor entender por que elas estão aqui para uma melhor utilização delas.

Por fim: uso getters porque uso muitas ferramentas de serialização, mas também não as escrevo; Eu uso o lombok: eu uso os atalhos fornecidos pelo meu IDE.

Walfrat
fonte
9
Ainda há valor na compreensão de expressões comuns encontradas em vários idiomas, pois elas se tornam um padrão de fato mais acessível. Esses idiomas tendem a se enquadrar nas "melhores práticas" por qualquer motivo.
precisa saber é o seguinte
3
Ei, uma solução sensata para o problema. Parece bastante razoável escrever apenas uma classe (/ struct) com alguns membros públicos, em vez de uma solução OOP totalmente projetada com engenharia avançada com etters [gs].
tomsmeding
3
Isso leva ao inchaço porque, diferentemente do Python, a pesquisa ou uma solução mais curta e elegante não é incentivada. Mas o lado positivo é que o inchaço será mais ou menos o mesmo tipo para qualquer desenvolvedor Java. Portanto, há um grau de maior capacidade de manutenção, pois os desenvolvedores são mais intercambiáveis ​​e, se dois projetos se unirem ou duas equipes divergirem involuntariamente, haverá menos diferença de estilo a ser combatida.
Mikhail Ramendik
2
@MikhailRamendik É uma troca entre YAGNI e OOP. OOP ortodoxo diz que todo objeto deve ser substituível; a única coisa que importa é a interface pública do objeto. Isso permite que você escreva códigos menos focados em objetos concretos e mais em interfaces; e como os campos não podem fazer parte de uma interface, você nunca os expõe. Isso pode facilitar muito a manutenção do código a longo prazo. Além disso, permite evitar estados inválidos. No seu exemplo original, é possível ter um objeto que seja "<ms" e "s", que são mutuamente exclusivos. Isso é ruim.
Luaan
2
@PeterMortensen É uma biblioteca Java que faz muitas coisas não relacionadas. É muito bom. Aqui estão os recursos
Michael
11

Sobre os idiomas Java em geral:

Existem várias razões pelas quais o Java tem classes para tudo. No que diz respeito ao meu conhecimento, o principal motivo é:

Java deve ser fácil de aprender para iniciantes. Quanto mais explícitas, mais difícil é perder detalhes importantes. Acontece menos mágica que seria difícil para os iniciantes entenderem.


Quanto ao seu exemplo específico: a linha de argumento para uma classe separada é a seguinte: se essas três coisas são suficientemente fortes uma para a outra, são retornadas como um valor, vale a pena nomear essa "coisa". E introduzir um nome para um grupo de coisas estruturadas de maneira comum significa definir uma classe.

Você pode reduzir o padrão com ferramentas como Lombok:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}
marstato
fonte
5
Também é o projeto AutoValue do Google: github.com/google/auto/blob/master/value/userguide/index.md
Ivan
É também por isso que os programas Java podem ser mantidos com
Thorbjørn Ravn Andersen
7

Há muitas coisas que podem ser ditas sobre a cultura Java, mas acho que, no caso de você se deparar agora, há alguns aspectos significativos:

  1. O código da biblioteca é escrito uma vez, mas usado com muito mais frequência. Embora seja bom minimizar a sobrecarga de escrever a biblioteca, provavelmente vale mais a pena a longo prazo, de forma a minimizar a sobrecarga de uso da biblioteca.
  2. Isso significa que os tipos de auto-documentação são ótimos: os nomes dos métodos ajudam a esclarecer o que está acontecendo e o que você está obtendo de um objeto.
  3. A digitação estática é uma ferramenta muito útil para eliminar certas classes de erros. Certamente não corrige tudo (as pessoas gostam de brincar com Haskell que, uma vez que você aceita o código do sistema, provavelmente está correto), mas facilita muito a impossibilidade de certos tipos de coisas erradas.
  4. Escrever código de biblioteca é sobre a especificação de contratos. A definição de interfaces para seus tipos de argumento e resultado torna os limites de seus contratos mais claramente definidos. Se algo aceita ou produz uma tupla, não há como dizer se é o tipo de tupla que você realmente deve receber ou produzir, e há muito pouco em termos de restrições em um tipo tão genérico (ele tem o número certo de elementos? eles do tipo que você estava esperando?).

Classes "Struct" com campos

Como outras respostas mencionaram, você pode simplesmente usar uma classe com campos públicos. Se você finalizá-las, obtém uma classe imutável e as inicializa com o construtor:

   class ParseResult0 {
      public final long millis;
      public final boolean isSeconds;
      public final boolean isLessThanOneMilli;

      public ParseResult0(long millis, boolean isSeconds, boolean isLessThanOneMilli) {
         this.millis = millis;
         this.isSeconds = isSeconds;
         this.isLessThanOneMilli = isLessThanOneMilli;
      }
   }

Obviamente, isso significa que você está vinculado a uma classe específica e qualquer coisa que precise produzir ou consumir um resultado de análise deve usar essa classe. Para algumas aplicações, tudo bem. Para outros, isso pode causar alguma dor. Grande parte do código Java é sobre a definição de contratos, e isso normalmente leva você a interfaces.

Outra armadilha é que, com uma abordagem baseada em classe, você expõe campos e todos esses campos devem ter valores. Por exemplo, isSeconds e millis sempre precisam ter algum valor, mesmo se isLessThanOneMilli for verdadeiro. Qual deve ser a interpretação do valor do campo millis quando isLessThanOneMilli for verdadeiro?

"Estruturas" como interfaces

Com os métodos estáticos permitidos nas interfaces, é realmente relativamente fácil criar tipos imutáveis ​​sem muita sobrecarga sintática. Por exemplo, eu posso implementar o tipo de estrutura de resultados que você está falando como algo assim:

   interface ParseResult {
      long getMillis();

      boolean isSeconds();

      boolean isLessThanOneMilli();

      static ParseResult from(long millis, boolean isSeconds, boolean isLessThanOneMill) {
         return new ParseResult() {
            @Override
            public boolean isSeconds() {
               return isSeconds;
            }

            @Override
            public boolean isLessThanOneMilli() {
               return isLessThanOneMill;
            }

            @Override
            public long getMillis() {
               return millis;
            }
         };
      }
   }

Ainda é um monte de clichê, eu concordo absolutamente, mas há alguns benefícios também, e acho que eles começam a responder algumas de suas principais perguntas.

Com uma estrutura como esse resultado da análise, o contrato do seu analisador é definido com muita clareza. No Python, uma tupla não é realmente diferente de outra. Em Java, a digitação estática está disponível; portanto, já descartamos determinadas classes de erros. Por exemplo, se você está retornando uma tupla em Python e deseja retornar a tupla (millis, isSeconds, isLessThanOneMilli), você pode acidentalmente fazer:

return (true, 500, false)

quando você quis dizer:

return (500, true, false)

Com esse tipo de interface Java, você não pode compilar:

return ParseResult.from(true, 500, false);

em absoluto. Você tem que fazer:

return ParseResult.from(500, true, false);

Esse é um benefício das linguagens estaticamente tipadas em geral.

Essa abordagem também começa a lhe dar a capacidade de restringir quais valores você pode obter. Por exemplo, ao chamar getMillis (), você pode verificar se isLessThanOneMilli () é verdadeiro e, se for, lançar uma IllegalStateException (por exemplo), já que não há valor significativo de millis nesse caso.

Tornando difícil fazer a coisa errada

No exemplo de interface acima, você ainda tem o problema de trocar acidentalmente os argumentos isSeconds e isLessThanOneMilli, pois eles têm o mesmo tipo.

Na prática, você pode realmente usar o TimeUnit e a duração, para obter um resultado como:

   interface Duration {
      TimeUnit getTimeUnit();

      long getDuration();

      static Duration from(TimeUnit unit, long duration) {
         return new Duration() {
            @Override
            public TimeUnit getTimeUnit() {
               return unit;
            }

            @Override
            public long getDuration() {
               return duration;
            }
         };
      }
   }

   interface ParseResult2 {

      boolean isLessThanOneMilli();

      Duration getDuration();

      static ParseResult2 from(TimeUnit unit, long duration) {
         Duration d = Duration.from(unit, duration);
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return false;
            }

            @Override
            public Duration getDuration() {
               return d;
            }
         };
      }

      static ParseResult2 lessThanOneMilli() {
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return true;
            }

            @Override
            public Duration getDuration() {
               throw new IllegalStateException();
            }
         };
      }
   }

Isso está ficando muito mais código, mas você só precisa escrevê-lo uma vez e (supondo que tenha documentado as coisas corretamente), as pessoas que acabam usando seu código não precisam adivinhar o que o resultado significa e acidentalmente não pode fazer coisas como result[0]quando elas significam result[1]. Você ainda pode criar instâncias de maneira bem sucinta, e obter dados delas também não é tão difícil:

  ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
  ParseResult2 y = ParseResult2.lessThanOneMilli();

Observe que você também pode fazer algo assim com a abordagem baseada em classe. Basta especificar construtores para os diferentes casos. No entanto, você ainda tem o problema de inicializar os outros campos e não pode impedir o acesso a eles.

Outra resposta mencionou que a natureza corporativa do Java significa que, na maioria das vezes, você está compondo outras bibliotecas que já existem ou escrevendo bibliotecas para outras pessoas usarem. Sua API pública não deve exigir muito tempo consultando a documentação para decifrar os tipos de resultado, se for possível evitar.

Você escreve essas estruturas apenas uma vez, mas as cria muitas vezes, portanto ainda deseja a criação concisa (que obtém). A digitação estática garante que os dados que você obtém deles sejam o que você espera.

Agora, tudo o que foi dito, ainda existem lugares onde tuplas ou listas simples podem fazer muito sentido. Pode haver menos sobrecarga no retorno de uma matriz de algo, e se esse for o caso (e essa sobrecarga for significativa, o que você determinaria com a criação de perfil), usar um conjunto simples de valores internamente pode fazer muito sentido. Sua API pública provavelmente ainda deve ter tipos claramente definidos.

Joshua Taylor
fonte
Usei minha própria enumeração em vez de TimeUnit no final, porque acabei de adicionar UNDER1MS junto com as unidades de tempo reais. Então agora eu só tenho dois campos, não três. (Em teoria, eu poderia abusar TimeUnit.NANOSECONDS, mas que seria muito confuso.)
Mikhail Ramendik
Eu não criei um getter, mas agora vejo como um getter me permitiria lançar uma exceção se, com originalTimeUnit=DurationTimeUnit.UNDER1MSo chamador, tentasse ler o valor de milissegundos.
Mikhail Ramendik
Eu sei que você mencionou, mas lembre-se de que seu exemplo com tuplas python é realmente sobre tuplas dinâmicas versus estáticas; realmente não diz muito sobre as tuplas em si. Você pode ter tuplas que não será compilado tipagem estática, como eu tenho certeza que você sabe :)
Andres F.
1
@Veedrac Não sei ao certo o que você quer dizer. Meu argumento foi que não há problema em escrever código de biblioteca de maneira mais detalhada (mesmo que demore mais), pois haverá mais tempo usando o código do que escrevendo .
27517 Joshua Taylor #
1
@Eedrac Sim, é verdade. Mas eu sustentam que não é uma distinção entre código que será significativamente reutilizados, e código que não vai. Por exemplo, em um pacote Java, algumas das classes são públicas e são usadas em outros locais. Essas APIs devem ser bem pensadas. Internamente, existem aulas que podem precisar apenas conversar entre si. Esses acoplamentos internos são os locais onde estruturas rápidas e sujas (por exemplo, um Object [] que se espera que tenha três elementos) podem estar bem. Para a API pública, algo mais significativo e explícito geralmente deve ser preferido.
Joshua Taylor
7

O problema é que você compara maçãs com laranjas . Você perguntou como simular o retorno de mais do que um valor único, fornecendo um exemplo rápido e sujo de python com tupla sem tipo e você realmente recebeu a resposta praticamente de uma linha .

A resposta aceita fornece uma solução comercial correta. Nenhuma solução temporária rápida que você precisaria jogar fora e implementar corretamente na primeira vez que precisaria fazer algo prático com o valor retornado, mas uma classe POJO compatível com um grande conjunto de bibliotecas, incluindo persistência, serialização / desserialização, instrumentação e tudo o que for possível.

Isso também não é demorado. A única coisa que você precisa escrever são as definições de campo. Setters, getters, hashCode e iguais podem ser gerados. Portanto, sua pergunta real deve ser: por que os getters e setters não são gerados automaticamente, mas é uma questão de sintaxe (questão sintática do açúcar, diriam alguns) e não a questão cultural.

E, finalmente, você está pensando demais tentando acelerar algo que não é importante. O tempo gasto na criação de classes de DTO é insignificante em comparação com o tempo gasto na manutenção e depuração do sistema. Portanto, ninguém otimiza para menos verbosidade.

Comunidade
fonte
2
"Ninguém" é factualmente incorreto - existem muitas ferramentas que otimizam para menos verbosidade, expressões regulares são um exemplo extremo. Mas você pode ter significado "ninguém no mundo principal do Java" e, nessa leitura, a resposta faz muito sentido, obrigado. Minha preocupação com a verbosidade não é o tempo gasto escrevendo código, mas o tempo gasto lendo-o; o IDE não economizará tempo de leitura; mas parece que a ideia é que 99% das vezes apenas se lê a definição da interface, para que isso seja conciso?
precisa saber é o seguinte
5

Existem três fatores diferentes que contribuem para o que você está observando.

Tuplas versus campos nomeados

Talvez o mais trivial - nos outros idiomas você usou uma tupla. Debater se as tuplas é uma boa ideia não é realmente o ponto - mas em Java você usou uma estrutura mais pesada, por isso é uma comparação um pouco injusta: você poderia ter usado uma variedade de objetos e algum tipo de conversão.

Sintaxe de idioma

Poderia ser mais fácil declarar a classe? Não estou falando sobre tornar os campos públicos ou usar um mapa, mas algo como as classes de caso do Scala, que fornecem todos os benefícios da configuração que você descreveu, mas são muito mais concisos:

case class Foo(duration: Int, unit: String, tooShort: Boolean)

Poderíamos ter isso - mas há um custo: a sintaxe se torna mais complicada. É claro que pode valer a pena para alguns casos, ou mesmo para a maioria dos casos, ou mesmo para a maioria dos casos nos próximos 5 anos - mas precisa ser julgado. A propósito, isso é uma das coisas legais com os idiomas que você pode modificar (ou seja, o lisp) - e observe como isso se torna possível devido à simplicidade da sintaxe. Mesmo que você não modifique o idioma, uma sintaxe simples permite ferramentas mais poderosas; por exemplo, muitas vezes sinto falta de algumas opções de refatoração disponíveis para Java, mas não para Scala.

Filosofia da linguagem

Mas o fator mais importante é que uma linguagem deve permitir uma certa maneira de pensar. Às vezes, pode parecer opressivo (eu sempre desejei suporte para um determinado recurso), mas a remoção de recursos é tão importante quanto tê-los. Você poderia apoiar tudo? Claro, mas você também pode escrever um compilador que compila todos os idiomas. Em outras palavras, você não terá um idioma - terá um superconjunto de idiomas e todo projeto adotaria basicamente um subconjunto.

É claro que é possível escrever um código que vá contra a filosofia da linguagem e, como você observou, os resultados geralmente são feios. Ter uma classe com apenas alguns campos em Java é semelhante a usar um var no Scala, degenerar predicados de prólogo para funções, executar umPerformIO inseguro em haskell etc. As classes Java não devem ser leves - elas não existem para passar dados . Quando algo parece difícil, geralmente é proveitoso dar um passo atrás e ver se há outra maneira. No seu exemplo:

Por que a duração é separada das unidades? Existem muitas bibliotecas de tempo que permitem declarar uma duração - algo como Duração (5 segundos) (a sintaxe varia), que permite fazer o que você quer de uma maneira muito mais robusta. Talvez você queira convertê-lo - por que verificar se o resultado [1] (ou [2]?) É uma 'hora' e se multiplica por 3600? E para o terceiro argumento - qual é o seu propósito? Eu acho que em algum momento você precisará imprimir "menos de 1ms" ou o tempo real - essa é uma lógica que naturalmente pertence aos dados de tempo. Ou seja, você deve ter uma classe como esta:

class TimeResult {
    public TimeResult(duration, unit, tooShort)
    public String getResult() {
        if tooShort:
           return "too short"
        else:
           return format(duration)
}

}

ou o que você realmente deseja fazer com os dados, encapsulando a lógica.

Claro, pode haver um caso em que esse caminho não funcione - não estou dizendo que esse é o algoritmo mágico para converter resultados de tuplas em código Java idiomático! E pode haver casos em que é muito feio e ruim e talvez você deva ter usado um idioma diferente - é por isso que existem tantos afinal!

Mas minha visão sobre por que as classes são "estruturas pesadas" em Java é que você não deve usá-las como contêineres de dados, mas como células da lógica independentes.

Thanos Tintinidis
fonte
O que eu quero fazer com as durações é realmente fazer uma soma delas e depois comparar com outra. Nenhuma saída envolvida. A comparação precisa levar em consideração o arredondamento, portanto, preciso conhecer as unidades de tempo originais no momento da comparação.
Mikhail Ramendik
Provavelmente seria possível criar uma maneira de adicionar e comparar instâncias da minha classe, mas isso pode ficar extremamente pesado. Especialmente porque eu também preciso dividir e multiplicar por carros alegóricos (há porcentagens para verificar nessa interface do usuário). Então, por enquanto, eu fiz uma aula imutável com os campos finais, o que parece um bom compromisso.
Mikhail Ramendik
A parte mais importante dessa questão em particular era o lado mais geral das coisas, e de todas as respostas uma imagem parece estar se juntando.
Mikhail Ramendik
@MikhailRamendik talvez algum tipo de acumulador seja uma opção - mas você conhece melhor o problema. Na verdade, não se trata de resolver esse problema em particular, desculpe-me se eu me desviei um pouco - meu ponto principal é que a linguagem desencoraja o uso de dados "simples". Às vezes, isso ajuda a repensar a sua abordagem - outras vezes um int é apenas um int
Thanos Tintinidis
@MikhailRamendik A coisa boa sobre a maneira mais convencional é que, depois de ter uma classe, você pode adicionar comportamento a ela. E / ou torná-lo imutável como você fez (essa é uma boa ideia na maioria das vezes; você sempre pode retornar um novo objeto como a soma). No final, sua classe "supérflua" pode encapsular todo o comportamento necessário e, em seguida, o custo para quem recebe fica insignificante. Além disso, você pode ou não precisar deles. Considere usar lombok ou autovalue .
Maaartinus 28/04
5

Na minha opinião, os principais motivos são

  1. As interfaces são a maneira básica do Java de abstrair as classes.
  2. Java pode retornar apenas um único valor de um método - um objeto ou uma matriz ou um valor nativo (int / long / double / float / boolean).
  3. As interfaces não podem conter campos, apenas métodos. Se você deseja acessar um campo, deve seguir um método - portanto, getters e setters.
  4. Se um método retornar uma interface, você deverá ter uma classe de implementação para realmente retornar.

Isso fornece a você "Você deve escrever uma classe para retornar a qualquer resultado não trivial", que por sua vez é bastante pesado. Se você usou uma classe em vez da interface, pode ter apenas campos e usá-los diretamente, mas isso o vincula a uma implementação específica.

Thorbjørn Ravn Andersen
fonte
Para adicionar a isso: se você precisa disso em um contexto pequeno (digamos, um método privado em uma classe), é bom usar um registro simples - idealmente imutável. Mas realmente nunca deveria vazar para o contrato público da classe (ou pior ainda, para a biblioteca!). Você pode decidir contra os registros simplesmente para garantir que isso nunca aconteça com futuras alterações de código, é claro; geralmente é a solução mais simples.
Luaan
E eu concordo com a visão "Tuplas não funcionam bem em Java". Se você usar genéricos, isso rapidamente acabará desagradável.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Eles funcionam muito bem em Scala, uma linguagem de tipo estático com genéricos e tuplas. Então, talvez isso seja culpa do Java?
Andres F.
@AndresF. Observe a parte "Se você usa genéricos". A maneira como o Java faz isso não se presta a construções complexas em oposição a, por exemplo, Haskell. Não conheço o Scala o suficiente para fazer uma comparação, mas é verdade que o Scala permite vários valores de retorno? Isso pode ajudar bastante.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen As funções Scala têm um único valor de retorno que pode ser do tipo tupla (por exemplo, def f(...): (Int, Int)é uma função fque retorna um valor que por acaso é uma tupla de números inteiros). Não tenho certeza se os genéricos em Java são o problema; observe que Haskell também digita apagamento, por exemplo. Eu não acho que haja uma razão técnica para o Java não ter tuplas.
Andres F.
3

Eu concordo com JacquesB responder que

Java é (tradicionalmente) uma linguagem OO baseada em classe de paradigma único que otimiza para explicitação e consistência

Mas explicitação e consistência não são os objetivos finais a serem otimizados. Quando você diz 'python é otimizado para facilitar a leitura', menciona imediatamente que o objetivo final é 'manutenibilidade' e 'velocidade de desenvolvimento'.

O que você alcança quando possui explicitação e consistência, do jeito Java? Minha opinião é que ela evoluiu como uma linguagem que pretende fornecer uma maneira previsível, consistente e uniforme de resolver qualquer problema de software.

Em outras palavras, a cultura Java é otimizada para fazer os gerentes acreditarem que entendem o desenvolvimento de software.

Ou, como um sábio disse há muito tempo ,

A melhor maneira de julgar um idioma é olhar para o código escrito por seus proponentes. "Radix enim omnium malorum est cupiditas" - e Java é claramente um exemplo de programação orientada a dinheiro (MOP). Como o principal proponente de Java na SGI me disse: "Alex, você precisa ir aonde está o dinheiro". Mas eu particularmente não quero ir aonde está o dinheiro - geralmente não cheira bem lá.

artem
fonte
3

(Esta resposta não é uma explicação para Java em particular, mas aborda a questão geral de “Para o que as práticas [pesadas] podem otimizar?”)

Considere estes dois princípios:

  1. É bom quando o seu programa faz a coisa certa. Deveríamos facilitar a gravação de programas que fazem a coisa certa.
  2. É ruim quando seu programa faz a coisa errada. Deveríamos tornar mais difícil escrever programas que fazem a coisa errada.

Tentar otimizar um desses objetivos pode atrapalhar o outro (por exemplo , dificultar a execução da coisa errada também pode dificultar a execução da coisa certa ou vice-versa).

Quais tradeoffs são feitos em qualquer caso específico depende do aplicativo, das decisões dos programadores ou da equipe em questão e da cultura (da organização ou da comunidade de idiomas).

Por exemplo, se um bug ou uma interrupção de algumas horas em seu programa resultar em perda de vidas (sistemas médicos, aeronáutica) ou até apenas dinheiro (como milhões de dólares nos sistemas de anúncios do Google), você faria trocas diferentes (não apenas no seu idioma, mas também em outros aspectos da cultura de engenharia) do que você faria em um roteiro único: provavelmente ele se inclina para o lado "pesado".

Outros exemplos que tendem a tornar seu sistema mais "pesado":

  • Quando você tem uma grande base de código trabalhada por muitas equipes ao longo de muitos anos, uma grande preocupação é que alguém possa usar a API de outra pessoa incorretamente. Uma função sendo chamada com argumentos na ordem errada ou sendo chamada sem garantir algumas pré-condições / restrições que ela espera, pode ser catastrófica.
  • Como um caso especial disso, diga que sua equipe mantém uma API ou biblioteca específica e gostaria de alterá-la ou refatorá-la. Quanto mais "restritos" seus usuários estiverem em como eles podem estar usando seu código, mais fácil será alterá-lo. (Observe que aqui seria preferível ter garantias reais de que ninguém poderia usá-lo de maneira incomum.)
  • Se o desenvolvimento for dividido entre várias pessoas ou equipes, pode parecer uma boa idéia que uma pessoa ou equipe “especifique” a interface e outras realmente a implementem. Para que funcione, é necessário obter um certo grau de confiança quando a implementação estiver concluída, de que a implementação realmente corresponde à especificação.

Estes são apenas alguns exemplos, para que você tenha uma idéia de casos em que tornar as coisas “pesadas” (e dificultar a escrita rápida de algum código) pode realmente ser intencional. (Pode-se até argumentar que, se escrever código requer muito esforço, isso pode levar você a pensar com mais cuidado antes de escrever código! É claro que essa linha de argumento rapidamente se torna ridícula.)

Um exemplo: o sistema Python interno do Google tende a tornar as coisas "pesadas", de modo que você não pode simplesmente importar o código de outra pessoa, você deve declarar a dependência em um arquivo BUILD , a equipe cujo código você deseja importar precisa ter sua biblioteca declarada como visível para o seu código etc.


Nota : Todas as opções acima são exatamente quando as coisas tendem a ficar "pesadas". Eu absolutamente não afirmo que Java ou Python (as próprias línguas ou suas culturas) fazem trocas ótimas para qualquer caso em particular; é para você pensar. Dois links relacionados a essas compensações:

ShreevatsaR
fonte
1
E a leitura de código? Há outra troca lá. É mais difícil ler códigos sobrecarregados por encantamentos bizarros e rituais profusores; com clichê e texto desnecessário (e hierarquias de classe!) no seu IDE, fica mais difícil ver o que o código está realmente fazendo. Percebo o argumento de que o código não deve ser necessariamente fácil de escrever (não tenho certeza se concordo com ele, mas tem algum mérito), mas definitivamente deve ser fácil de ler . Obviamente, códigos complexos podem e foram criados com Java - seria tolice afirmar o contrário - mas foi graças à sua verbosidade ou apesar disso?
Andres F.
@AndresF. Editei a resposta para deixar mais claro que não se trata especificamente de Java (do qual não sou um grande fã). Mas sim, vantagens e desvantagens ao ler código: em um extremo, você deseja ler facilmente “o que o código está realmente fazendo”, como você disse. No outro extremo, você deseja ver facilmente respostas a perguntas como: como esse código se relaciona com outro código? O pior que esse código pode fazer: qual pode ser o impacto em outro estado, quais são seus efeitos colaterais? De quais fatores externos esse código poderia depender? (É claro que idealmente queremos respostas rápidas para os dois conjuntos de perguntas.)
ShreevatsaR
2

A cultura Java evoluiu ao longo do tempo com fortes influências de fontes abertas e de software corporativo - o que é uma mistura estranha se você realmente pensa sobre isso. As soluções corporativas exigem ferramentas pesadas e o código aberto exige simplicidade. O resultado final é que Java está em algum lugar no meio.

Parte do que está influenciando a recomendação é o que é considerado legível e de manutenção em Python e Java é muito diferente.

  • No Python, a tupla é um recurso de linguagem .
  • Em Java e C #, a tupla é (ou seria) um recurso de biblioteca .

Menciono apenas C # porque a biblioteca padrão possui um conjunto de classes Tuple <A, B, C, .. n> e é um exemplo perfeito de como as tuplas são difíceis de manusear, se o idioma não as suportar diretamente. Em quase todos os casos, seu código se torna mais legível e de manutenção se você tiver escolhido classes para lidar com o problema. No exemplo específico da sua pergunta de estouro de pilha vinculada, os outros valores seriam facilmente expressos como getters calculados no objeto de retorno.

Uma solução interessante que a plataforma C # fez, que fornece um meio termo feliz, é a idéia de objetos anônimos (lançados no C # 3.0 ) que arranham essa coceira muito bem. Infelizmente, Java ainda não tem um equivalente.

Até que os recursos da linguagem Java sejam alterados, a solução mais legível e de manutenção é ter um objeto dedicado. Isso ocorre devido a restrições na linguagem que datam de 1995. Os autores originais tinham muito mais recursos de linguagem planejados que nunca foram criados, e a compatibilidade com versões anteriores é uma das principais restrições em torno da evolução do Java ao longo do tempo.

Berin Loritsch
fonte
4
Na versão mais recente do c #, as novas tuplas são cidadãos de primeira classe e não de biblioteca. Foram necessárias muitas versões para chegar lá.
Igor Soloydenko
Os últimos 3 parágrafos podem ser resumidos como "A linguagem X possui o recurso que você procura que o Java não possui" e não consigo ver o que eles adicionam à resposta. Por que mencionar C # em um tópico Python para Java? Não, eu simplesmente não vejo o ponto. Um pouco estranho de um cara de 20k + representantes.
Olivier Grégoire
1
Como eu disse, "eu apenas mencionei o C # porque as Tuplas são um recurso de biblioteca", semelhante a como isso funcionaria em Java se tivesse Tuplas na biblioteca principal. É muito difícil de usar, e a melhor resposta é quase sempre ter uma aula dedicada. Os objetos anônimos arranham uma coceira semelhante à que as tuplas são projetadas, no entanto. Sem o suporte ao idioma para algo melhor, você basicamente precisa trabalhar com o que é mais sustentável.
Berin Loritsch
@IgorSoloydenko, talvez haja esperança para o Java 9? Esse é apenas um desses recursos que são o próximo passo lógico para as lambdas.
precisa saber é o seguinte
1
@IgorSoloydenko Não tenho certeza se isso é verdade (cidadãos de primeira classe) - eles parecem extensões de sintaxe para criar e desembrulhar instâncias de classes da biblioteca, em vez de ter suporte de primeira classe no CLR como class, struct, enum do.
Pete Kirkham
0

Eu acho que uma das principais coisas sobre o uso de uma classe neste caso é que o que acontece junto deve permanecer junto.

Eu tive essa discussão ao contrário, sobre argumentos de método: Considere um método simples que calcula o IMC:

CalculateBMI(weight,height)
{
  System.out.println("BMI: " + (( weight / height ) x 703));
}

Nesse caso, eu argumentaria contra esse estilo porque peso e altura estão relacionados. O método "comunica" esses são dois valores separados quando não estão. Quando você calcularia um IMC com o peso de uma pessoa e a altura de outra? Não faria sentido.

CalculateBMI(Person)
{
  System.out.println("BMI: " + (( Person.weight / Person.height ) x 703));
}

Faz muito mais sentido, porque agora você comunica claramente que a altura e o peso vêm da mesma fonte.

O mesmo vale para o retorno de vários valores. Se eles estiverem claramente conectados, retorne um pequeno pacote e use um objeto, se não retornar vários valores.

Pieter B
fonte
4
Ok, mas suponha que você tenha uma tarefa (hipotética) para mostrar um gráfico: como o IMC depende do peso para uma determinada altura fixa. Então, você não pode fazer isso sem criar uma Pessoa para cada ponto de dados. E, levando ao extremo, você não pode criar uma instância da classe Person sem data de nascimento e número de passaporte válidos. O que agora? Eu não downvote btw, não são razões válidas para a agregação propriedades juntos em alguma classe.
Artem
1
@artem é o próximo passo em que as interfaces entram em jogo. Portanto, uma interface de peso por pessoa pode ser algo assim. Você está sendo pego no exemplo que dei para enfatizar que o que acontece junto deve estar junto.
Pieter B
0

Para ser franco, a cultura é que os programadores de Java tendiam originalmente a sair de universidades onde eram ensinados princípios orientados a objetos e princípios de design de software sustentável.

Como ErikE diz em mais palavras em sua resposta, você parece não estar escrevendo código sustentável. O que vejo no seu exemplo é que há uma confusão muito incômoda de preocupações.

Na cultura Java, você tenderá a estar ciente de quais bibliotecas estão disponíveis, e isso permitirá que você obtenha muito mais do que sua programação imediata. Portanto, você trocaria suas idiossincrasias pelos padrões e estilos de design que foram experimentados e testados em ambientes industriais de núcleo duro.

Mas, como você diz, isso não tem desvantagens: hoje, tendo usado Java há mais de 10 anos, costumo usar Node / Javascript ou Go para novos projetos, porque ambos permitem um desenvolvimento mais rápido e, com arquiteturas no estilo de microsserviço, são muitas vezes é suficiente. A julgar pelo fato de o Google ter usado muito Java, mas foi o criador do Go, acho que eles podem estar fazendo o mesmo. Mas, embora agora use Go e Javascript, ainda uso muitas das habilidades de design que adquiri em anos de uso e compreensão de Java.

Tom
fonte
1
Notavelmente, a ferramenta de recrutamento foobar usada pelo Google permite que dois idiomas sejam usados ​​para soluções: java e python. Acho interessante que o Go não seja uma opção. Eu deparei com esta apresentação no Go e ela tem alguns pontos interessantes sobre Java e Python (assim como outras linguagens.) Por exemplo, há um ponto destacado: "Talvez como resultado, programadores não especialistas tenham confundido" a facilidade de use "com interpretação e digitação dinâmica". Vale a pena ler.
JimmyJames
@JimmyJames ficou para o slide dizendo 'nas mãos de especialistas, eles são ótimos' - bom resumo do problema na questão
Tom