杂乱无章的Java文化-为什么事情如此繁重?它优化了什么?[关闭]


288

我以前经常用Python编写代码。现在,出于工作原因,我使用Java编写代码。我从事的项目相当小,也许Python会更好地工作,但是有使用Java的非工程性正当理由(我无法详述)。

Java语法没问题;这只是另一种语言。但是,除了语法外,Java还具有被视为“正确”的文化,一套开发方法和实践。现在,我完全无法“培养”这种文化。因此,我将不胜感激正确方向的解释或指点。

我开始的堆栈溢出问题中提供了一个最小的完整示例:https//stackoverflow.com/questions/43619566/returning-a-result-with-several-values-the-java-way/43620339

我有一个任务-解析(从单个字符串)并处理一组三个值。在Python中,它是一个单行(元组),在Pascal或C中,它是一个五行记录/结构。

根据答案,可以在Java语法中使用struct的等效项,在广泛使用的Apache库中使用struct的等效项-但是“正确”的实现方法实际上是通过为值创建一个单独的类,并完成吸气剂和二传手。有人很乐意提供一个完整的例子。它是47行代码(嗯,其中一些行是空白)。

我知道,庞大的开发社区可能并非“错误”。因此,根据我的理解,这是一个问题。

Python实践优化了可读性(按照这种哲学,导致了可维护性),然后提高了开发速度。C惯例针对资源使用进行了优化。Java实践针对什么进行了优化?我最好的猜测是可伸缩性(一切都应准备好以百万计的LOC项目的状态),但这是一个非常微不足道的猜测。


1
不会回答问题的技术方面,但仍在上下文中:softwareengineering.stackexchange.com/a/63145/13899
Newtopian '17

74
您可以享受史蒂夫·耶格的文章执行王国名词
上校恐慌


3
我认为这是正确的答案。您并不是在厌烦Java,因为您考虑的是像sysadmin这样的程序而不是程序员。对您而言,软件是您用来以最快的方式完成特定任务的工具。我已经编写Java代码已有20年了,而我从事的某些项目需要20、2年的团队才能完成。Java不能替代Python,反之亦然。他们俩都做工作,但是他们的工作完全不同。
理查德

1
Java是事实上的标准的原因是因为它完全正确。它是第一次由认真的人创建的,这些人在胡须时髦之前已经留了胡须。如果您习惯于敲击Shell脚本来操作文件,那么Java似乎难以忍受。但是,如果您要创建一个双冗余服务器群集,该群集每天能够为5000万用户提供服务,并由群集的Redis缓存,3个计费系统和一个2000万磅的oracle数据库群集作为后盾。 Python中的数据库少了25行代码。
理查德

Answers:


237

Java语言

我相信所有这些答案都试图通过将意图归因于Java的工作方式而忽略了重点。Java的冗长性并非源于面向对象,因为Python和许多其他语言也都具有更短的语法。Java的冗长性也不来自于对访问修饰符的支持。相反,这只是Java的设计和发展方式。

Java最初是用OO稍微改进后的C语言创建的。因此,Java具有70年代时代的语法。此外,Java在添加功能方面非常保守,以保持向后兼容性并使其经受住时间的考验。Java在2005年添加了诸如XML文字的流行功能时,当时XML一直风靡一时,而该语言本来会因为没有人关心的幽灵功能而and肿,并在10年后限制了它的发展。因此,Java根本缺少很多现代语法来简洁地表达概念。

但是,没有什么基本的方法可以阻止Java采用该语法。例如,Java 8添加了lambda和方法引用,从而在许多情况下大大降低了冗长性。Java可以类似地添加对紧凑数据类型声明(例如Scala的case类)的支持。但是Java根本没有这样做。请注意,自定义值类型即将出现,此功能可能会引入新的语法来声明它们。我想我们会看到的。


Java文化

企业Java开发的历史很大程度上将我们带入了当今的文化。在90年代末/ 00年代初,Java成为服务器端业务应用程序的一种非常流行的语言。那时,这些应用程序大多是临时编写的,并包含了许多复杂的问题,例如HTTP API,数据库和处理XML提要。

到了20世纪90年代,很明显,许多应用程序具有许多共同点和框架来管理这些问题,例如Hibernate ORM,Xerces XML解析器,JSP和Servlet API以及EJB变得流行。但是,尽管这些框架减少了在它们设置为自动化的特定域中工作的精力,但它们仍需要配置和协调。当时,无论出于何种原因,编写框架来迎合最复杂的用例都是很流行的,因此这些库的设置和集成都很复杂。随着时间的推移,它们随着功能的积累而变得越来越复杂。Java企业的发展逐渐变得越来越多地将第三方库集成在一起,而不再需要编写算法。

最终,繁琐的企业工具配置和管理变得十分痛苦,以至于出现了框架(最著名的是Spring框架)来管理管理。从理论上讲,您可以将所有配置放在一个位置,然后配置工具将配置零件并将它们连接在一起。不幸的是,这些“框架框架” 在整个蜡球之上增加了更多的抽象性和复杂性

在过去的几年中,越来越多的轻量级库变得越来越流行。但是,随着重型企业框架的发展,整代Java程序员都已经成熟。他们的角色模型(开发框架的模型)编写了工厂工厂和代理配置Bean加载程序。他们必须每天配置和集成这些怪兽。结果,整个社区的文化都遵循了这些框架的榜样,并且往往过度设计。


15
有趣的是,其他语言似乎正在采用一些“ java-ism”。PHP OO功能在很大程度上受到Java的启发,如今,JavaScript因其庞大的框架以及收集所有依赖项并启动新应用程序的难度而受到批评。
marcus

14
@marcus也许是因为某些人终于学会了“不要重新发明轮子”的东西?毕竟,依赖是不重新发明轮子的代价
Walfrat

9
“ Terse”通常会翻译成奥秘的“聪明”,代码高尔夫般的单线。
图兰斯·科尔多瓦

7
周到,精心编写的答案。历史背景。相对没有意见。做得很好。
Cheezmeister

10
@TulainsCórdova我同意我们应该“避免...像瘟疫一样的聪明把戏”(Donald Knuth),但这并不意味着for在a map更合适(且易于理解)时编写循环。有一个快乐的媒介。
Brian McCutchon '17

73

对于您提出的其他人没有提出的观点之一,我相信我有一个答案。

Java从不进行任何假设,从而优化了编译时程序员的错误检测

通常,Java倾向于仅在程序员已经明确表达其意图之后推断有关源代码的事实。Java编译器从不对代码做任何假设,而只会使用推理来减少冗余代码。

这种理念背后的原因是程序员只是人。我们编写的内容并不总是我们打算实际执行的程序。Java语言试图通过迫使开发人员始终显式声明其类型来缓解其中的某些问题。这只是仔细检查所编写的代码是否确实达到了预期目的的一种方式。

其他一些语言通过检查前置条件,后置条件和不变量来进一步推动该逻辑的发展(尽管我不确定它们是在编译时执行的)。对于程序员来说,这些甚至是更极端的方式,可以使编译器再次检查自己的工作。

在您的情况下,这意味着为了使编译器保证您实际上返回的是您认为要返回的类型,您需要将该信息提供给编译器。

在Java中,有两种方法可以做到这一点:

  1. 使用Triplet<A, B, C>的返回类型(这确实应该是java.util,我真的不能解释为什么它不是。尤其是自JDK8介绍FunctionBiFunctionConsumerBiConsumer,等...这似乎只是PairTriplet至少会是有意义的。但是,我离题)

  2. 为此,请创建您自己的值类型,在其中正确命名和键入每个字段。

在这两种情况下,编译器都可以保证您的函数返回其声明的类型,并且调用者可以意识到每个返回字段的类型是什么,并相应地使用它们。

某些语言确实同时提供了静态类型检查和类型推断,但这为微妙的类型不匹配问题类打开了大门。如果开发人员打算返回某个类型的值,但实际上却返回了另一个类型,而编译器仍然接受该代码,因为碰巧该函数和调用者都碰巧只能同时使用可应用于预期值的方法和实际类型。

在Typescript(或流类型)中考虑类似这种情况,其中使用类型推断而不是显式键入。

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);

当然,这是一个愚蠢的琐碎情况,但由于代码看起来正确,因此它可能更加微妙,并导致难以调试问题。用Java完全避免了这种问题,这就是整个语言的设计目的。


请注意,遵循正确的面向对象原则和领域驱动的建模,链接问题中的持续时间分析案例也可以作为java.time.Duration对象返回,这比上述两种情况都更加明确。


38
声明Java为代码正确性进行了优化可能是一个正确的观点,但是在我看来(编程许多语言,最近使用了一些Java),该语言要么失败要么效率极低。诚然,Java很老,那时还不存在很多东西,但是有其他现代替代方法可以提供更好的正确性检查,例如Haskell(我敢说吗?Rust或Go)。Java的所有笨拙对于此目标都是不必要的。-除此之外,这不说明了文化,但Solomonoff透露,文化是BS反正。
tomsmeding

8
该示例并没有证明类型推断就是问题,只是JavaScript决定允许使用动态语言进行隐式转换是愚蠢的。任何具有类型推断的理智的动态语言或静态吹捧的语言都不允许这样做。作为这两种示例:python会抛出一个运行时例外,Haskell不会编译。
Voo

39
@tomsmeding值得注意的是,这种说法特别愚蠢,因为Haskell的存在时间比Java 了5年。Java可能具有类型系统,但在发布时甚至没有最新的正确性验证。
亚历克西斯·金

21
“ Java针对编译时错误检测进行了优化” —否。它提供了它,但是,正如其他人所指出的那样,从任何意义上讲,它当然都没有针对它进行优化。此外,这种说法是完全不相关的,以OP的问题,因为其他语言确实优化编译时错误检测有类似的情景更轻量级语法。Java过于冗长的冗长与编译时验证完全无关
Konrad Rudolph

19
@KonradRudolph是的,虽然我认为它在情感上很吸引人,但这个答案是完全荒谬的。我不确定为什么会有这么多的选票。与Java相比,Haskell(或更重要的是Idris)在准确性方面的优化远远超过Java,它具有具有轻量级语法的元组。更重要的是,定义数据类型是单线的,您将获得Java版本将获得的一切。这个答案是糟糕的语言设计和不必要的冗长性的借口,但是如果您喜欢Java并且不熟悉其他语言,这听起来不错。
亚历克西斯·金

50

Java和Python是我使用最多的两种语言,但我来自另一个方向。也就是说,在开始使用Python之前,我对Java领域很了解,因此我可能会提供帮助。我认为,“为什么这么重”这个更大的问题的答案可以归结为两点:

  • 两者之间的开发成本就像长气球动物气球中的空气一样。您可以挤压气球的一部分,而另一部分则膨胀。Python倾向于挤压早期部分。Java压缩后一部分。
  • Java仍然缺少一些功能,可以减轻一些负担。Java 8在这方面起了很大的作用,但是这种文化尚未完全消化这些变化。Java可以使用其他一些东西,例如yield

Java“优化”了高价值软件,该软件将由大型团队维护多年。我曾经用Python编写东西并在一年后看它的经验,对自己的代码感到困惑。在Java中,我可以查看其他人的代码的小片段,并立即知道它的作用。在Python中,您无法真正做到这一点。正如您似乎意识到的那样,这并不是一个更好的选择,而是它们具有不同的成本。

在您提到的特定情况下,没有元组。一种简单的解决方案是创建一个具有公共价值的类。Java刚问世时,人们经常这样做。这样做的第一个问题是维护麻烦。如果您需要增加一些逻辑或线程安全性或想要使用多态性,则至少需要触摸与该“元组式”对象进行交互的每个类。在Python中,有诸如此类的解决方案,__getattr__因此并不是那么可怕。

虽然有一些不良习惯(IMO)。在这种情况下,如果您想要一个元组,我会问为什么要将它设为可变对象。您只需要使用getter(附带说明,我讨厌get / set约定,但这就是它的意思。)我确实认为裸类(是否可变)在Java的私有或程序包私有上下文中很有用。 。也就是说,通过将项目中的引用限制为该类,可以稍后根据需要进行重构,而无需更改类的公共接口。这是一个如何创建简单的不可变对象的示例:

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;
  }
}

这是我使用的一种模式。如果您不使用IDE,则应该开始。它将为您生成吸气剂(如果需要,也可以生成吸气剂),所以这并不是那么痛苦。

如果我不指出的是,已经有这似乎满足你的大部分需求的类型我会觉得失职这里。抛开这些,您使用的方法不太适合Java,因为它发挥了弱点,而不是长处。这是一个简单的改进:

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;
  }
}

评论不作进一步讨论;此对话已转移至聊天
maple_shaft

2
我是否可以建议您也关注Scala有关“更现代”的Java功能...
MikeW

1
@MikeW实际上,我早在早期版本中就从Scala开始的,并在scala论坛上活跃了一段时间。我认为这是语言设计方面的一项伟大成就,但我得出的结论是,这并不是我真正想要的,而且我是那个社区中的钉子。我应该再看一遍,因为自那时以来,它可能已经发生了重大变化。
JimmyJames

该答案未解决OP提出的近似值方面。
ErikE

@ErikE问题文本中未出现单词“近似值”。如果您可以指出我未解决的问题的特定部分,则可以尝试这样做。
JimmyJames

41

在您对Java过于生气之前,请阅读您在其他文章中的回答

您的抱怨之一是需要创建一个类来返回一些值作为答案。我认为这是一个有效的担忧,表明您的编程直觉是正确的!但是,我认为其他答案由于坚持您所致力于的原始痴迷反模式而没有实现。而且Java使用多个原语的便利性不像Python那样简单,在Python中,您可以本地返回多个值并将它们轻松分配给多个变量。

但是,一旦您开始考虑ApproximateDuration类型对您有什么作用,您就会意识到,它的范围并不局限于“仅仅一个看似不必要的类以返回三个值”。此类所代表的概念实际上是您的核心领域业务概念之一 -需要能够以近似的方式表示时间并进行比较。它必须是应用程序核心通用语言的一部分,并具有良好的对象和域支持,以便可以对其进行测试,模块化,可重用和有用。

您的代码是将近似的持续时间(或持续时间有误差的总和,还是您所表示的持续时间)加在一起完全是程序性的吗?我建议,围绕近似持续时间求和的良好设计将指示在可以测试的类中的任何消耗代码之外进行此设计。我认为使用这种域对象将在您的代码中产生积极的连锁反应,从而帮助您摆脱逐行的程序步骤来完成单个高级任务(尽管有很多责任),而转向单一职责类摆脱了不同关注点的冲突。

例如,假设您了解更多有关持续时间求和和比较正常工作实际需要的精度或小数位数的信息,并且发现需要一个中间标记来指示“大约32毫秒错误”(靠近平方1000的根,因此在1到1000之间进行对数对数)。如果您将自己绑定到使用基元表示此代码的代码,则必须在代码中查找您拥有的每个位置is_in_seconds,is_under_1ms并将其更改为is_in_seconds,is_about_32_ms,is_under_1ms。到处都会改变一切!安排一个班级来负责记录误差范围,以便可以在其他地方使用它,这可以使您的消费者无需了解什么误差范围很重要或如何组合的细节,而是让他们只指定相关的误差范围在这一刻。(也就是说,当您在类中添加新的错误裕度级别时,不会强制更改其错误裕度正确的使用方代码,因为所有旧的错误裕度仍然有效)。

结束语

随着您逐渐接近SOLID和GRASP的原理以及更高级的软件工程,关于Java繁重的抱怨似乎消失了。

附录

我将完全免费且不公平地补充一点,C#的自动属性和在构造函数中分配仅获取属性的能力有助于进一步清理“ Java方式”所需的有点凌乱的代码(具有显式的私有支持字段和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; }
}

这是上述内容的Java实现:

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;
  }
}

现在,这真是太干净了。请注意不变性的非常重要和有意的使用-这对于这种特殊的具有价值的课程来说似乎至关重要。

因此,此类也是a struct,值类型的一个不错的选择。一些测试将显示切换到结构是否具有运行时性能优势(可能)。


9
我认为这是我最喜欢的答案。我发现,当我将这种a脚的一次性持有人阶级提升为成年后的人时,它将成为承担所有相关职责的家园!一切都变得更加清洁!而且我通常同时会学习一些有关问题空间的有趣信息。显然,有一种避免过度设计的技巧……但是,当Gosling忽略了元组语法时(与GOTO一样),他并不愚蠢。您的总结发言非常出色。谢谢!
SusanW

2
如果您喜欢此答案,请阅读域驱动设计。关于用代码表示领域概念的这个话题有很多话要说。
neontapir

@neontapir是的,谢谢!我知道有个名字!:-)虽然我必须说,当领域概念通过一些启发性的重构有机地出现时,我更喜欢它(像这样!)……这有点像当您通过发现海王星来解决19世纪重力问题时。 。
SusanW

@SusanW我同意。重构以消除原始的困扰是发现领域概念的绝妙方法!
neontapir

我添加了该示例的Java版本。我没有掌握C#中所有不可思议的语法功能,因此,如果缺少某些内容,请告诉我。
JimmyJames

24

Python和Java均根据其设计者的理念针对可维护性进行了优化,但是对于如何实现这一点,他们有非常不同的想法。

Python是一种多范式语言,它优化了代码的清晰度简洁性(易于读写)。

Java是(传统上)基于单范例类的OO语言,它优化了显式性一致性 -甚至以更冗长的代码为代价。

Python元组是具有固定数量字段的数据结构。带有显式声明的字段的常规类可以实现相同的功能。在Python中,提供元组作为类的替代品是很自然的,因为它可以极大地简化代码,尤其是由于内置了对元组的语法支持。

但这与提供这种快捷方式的Java文化并不完全匹配,因为您已经可以使用显式声明的类。无需引入其他类型的数据结构,只是节省了一些代码行并避免了一些声明。

Java倾向于使用最少的特殊情况语法糖来统一应用的单个概念(类),而Python提供了多种工具和大量语法糖,使您可以针对任何特定目的选择最方便的方法。


+1,但是如何与带有类的静态类型语言(例如Scala)相结合,却发现需要引入元组?我认为在某些情况下,元组只是更整洁的解决方案,即使对于带有类的语言也是如此。
Andres F.

@AndresF .:网格是什么意思?Scala是一种不同于Java的语言,具有不同的设计原理和习惯用法。
JacquesB

我是在回答您的第5段时用这个词的,它的开头是“但这与Java文化并不完全匹配”。确实,我同意冗长是Java文化的一部分,但是缺少元组不是因为它们“已经使用了显式声明的类”-毕竟,Scala也有显式类(并引入了更简洁的case类),但是它还允许元组!最后,我认为Java可能没有引入元组不仅是因为类可以达到相同的目的(更加混乱),而且还因为C程序员必须熟悉其语法,并且C没有元组。
Andres F.

@AndresF。:Scala并不反对多种处理方式,它结合了功能范例和经典OO的功能。这样更接近于Python哲学。
JacquesB

@AndresF .:是的,Java的语法接近于C / C ++,但是简化了很多。C#最初几乎是Java的精确副本,但多年来增加了很多功能,包括元组。因此,这确实是语言设计理念上的差异。(尽管Java的较新版本似乎没有那么教条。最初可以拒绝使用高阶函数,因为您可以使用相同的类,但现在已经引入了它们。所以也许我们看到了哲学上的改变。)
JacquesB

16

不要寻找做法;如最佳实践BAD中所述,这通常是一个坏主意,模式好吗?。我知道您并没有要求最佳实践,但我仍然认为您会在其中找到一些相关的要素。

寻找问题的解决方案比实践更好,并且您的问题不是在Java中快速返回三个值的元组:

  • 有数组
  • 您可以将一个数组作为一个列表中的列表返回:Arrays.asList(...)
  • 如果您想保留尽可能少的样板(且不带龙目岛),则可以:

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,....){
        ...
    }
 }

在这里,您有一个不可变的对象,只包含您的数据,并且是公共的,因此不需要获取方法。请注意,但是,如果您使用某些序列化工具或持久性层(如ORM),则它们通常使用getter / setter(并且可以接受参数以使用字段而不是getter / setter)。这就是为什么这些做法被广泛使用的原因。因此,如果您想了解实践,最好了解它们为什么在这里,以便更好地使用它们。

最后:我使用吸气剂是因为我使用了很多序列化工具,但是我都不编写它们。我使用lombok:我使用IDE提供的快捷方式。


9
理解各种语言中常见的习惯用法仍然有价值,因为它们已成为事实上更易于访问的标准。无论出于何种原因,这些习惯用法都倾向于落入“最佳实践”的标题之下。
Berin Loritsch '17

3
嘿,解决这个问题的明智方法。编写一个只有几个公共成员的类(/ struct)而不是使用[gs] etters的成熟的,过度设计的OOP解决方案似乎很合理。
tomsmeding

3
这确实会导致膨胀,因为与Python不同的是,不鼓励使用搜索或更简短,更优雅的解决方案。但是好处是,对于任何Java开发人员而言,膨胀或多或少都是相同的。因此,由于开发人员之间的互换性更高,因此在一定程度上具有更高的可维护性,并且如果两个项目被加入或者两个团队无意间发生分歧,那么战斗的风格差异就更少了。
Mikhail Ramendik '17

2
@MikhailRamendik这是YAGNI和OOP之间的权衡。正统的OOP表示每个对象都应该是可替换的。唯一重要的是对象的公共接口。这使您可以编写较少关注具体对象而更多关注接口的代码。并且由于字段不能成为接口的一部分,因此您永远不会公开它们。从长远来看,这可以使代码更易于维护。另外,它允许您防止无效状态。在您的原始示例中,可能有一个同时为“ <ms”和“ s”的对象。那很糟。
a安

2
@PeterMortensen这是一个Java库,它执行许多完全不相关的事情。很好。这里是功能
Michael

11

一般而言,关于Java习惯用语:

Java拥有所有类的类有多种原因。据我所知,主要原因是:

Java对于初学者来说应该很容易学习。事情越明确,就越难以错过重要的细节。较少发生的魔术对于初学者来说很难掌握。


对于您的特定示例:一个单独类的参数行是这样的:如果这三件事彼此之间有足够的关联,并且它们作为一个值返回,则值得将其命名为“事物”。为以通用方式构造的一组事物引入名称意味着定义一个类。

您可以使用Lombok之类的工具来简化样板:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}

5
Ivan

这也是为什么Java程序可以相对容易维护的原因!
托尔比约恩Ravn的安徒生

7

关于Java文化,可以说很多事情,但是我认为在您现在正面临的情况下,有一些重要方面:

  1. 库代码只编写一次,但使用频率更高。虽然可以最大程度地减少编写库的开销,但是从长远来看,以使使用库的开销降至最低的方式进行编写可能更值得。
  2. 这意味着自我记录类型非常有用:方法名称有助于清楚地说明正在发生的事情以及您从对象中得到的结果。
  3. 静态类型化是消除某些错误类别的非常有用的工具。它当然不能解决所有问题(人们喜欢开玩笑说Haskell,一旦您使类型系统接受您的代码,那可能是正确的),但是它使得很容易使某些错误的事情变得不可能。
  4. 编写库代码是关于指定合同的。为参数和结果类型定义接口可以使合同的界限更加明确。如果某个东西接受或产生一个元组,则不必说这是您实际上应该接收还是产生的元组,并且对这样的泛型类型的约束很少(它甚至具有正确数量的元素吗?他们是您期望的类型?)。

具有字段的“结构”类

正如其他答案所提到的,您可以只使用带有公共字段的类。如果将它们定型,则将获得一个不可变的类,并使用构造函数对其进行初始化:

   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;
      }
   }

当然,这意味着您与特定的类相关联,任何需要产生或使用解析结果的内容都必须使用该类。对于某些应用程序,这很好。对于其他人,这可能会引起一些痛苦。许多Java代码都是关于定义合同的,通常会将您带入接口。

另一个陷阱是,使用基于类的方法时,您将暴露字段,并且所有这些字段都必须具有值。例如,即使isLessThanOneMilli为true,isSeconds和millis始终必须具有某些值。当isThanOneMilli为true时,对Millis字段的值的解释应该是什么?

“结构”作为接口

使用接口中允许使用的静态方法,实际上相对容易地创建不可变的类型,而没有大量的语法开销。例如,我可能会像这样实现您正在谈论的结果结构:

   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;
            }
         };
      }
   }

我绝对同意,这仍然是很多样板,但是也有很多好处,而且我认为那些开始回答您的一些主要问题。

使用这种解析结果的结构,可以非常清楚地定义解析器的协定。在Python中,一个元组与另一个元组并没有真正的区别。在Java中,静态类型可用,因此我们已经排除了某些类型的错误。例如,如果您要使用Python返回一个元组,并且想要返回该元组(millis,isSeconds,isLessThanOneMilli),则可能会意外地执行以下操作:

return (true, 500, false)

当你的意思是:

return (500, true, false)

使用这种Java接口,您将无法编译:

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

完全没有 你必须做:

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

通常,这是静态类型语言的好处。

这种方法也开始使您能够限制可以获取的值。例如,在调用getMillis()时,您可以检查isLessThanOneMilli()是否为true,如果是,则抛出IllegalStateException(例如),因为在这种情况下没有有意义的millis值。

很难做错事

在上面的接口示例中,您仍然会遇到问题,因为它们具有相同的类型,因此可能会意外交换isSeconds和isLessThanOneMilli参数。

在实践中,您实际上可能真的想利用TimeUnit和持续时间,以便得到类似以下的结果:

   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();
            }
         };
      }
   }

这越来越成为一个很多更多的代码,但是你只需要编写一次,和(假设你已经正确记录的东西),谁落得人使用你的代码没有什么结果意味着猜了,不能偶然地做那些result[0]刻薄的事情result[1]。您仍然可以非常简洁地创建实例,并且从其中获取数据也不是那么困难:

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

请注意,您实际上也可以使用基于类的方法执行类似的操作。只需为不同情况指定构造函数。但是,仍然存在将其他字段初始化为什么的问题,并且无法阻止对它们的访问。

另一个答案提到Java的企业类型性质意味着在很多时候,您是在组合其他已经存在的库,或者编写供其他人使用的库。你的公共API不应该需要大量的时间咨询文件破译结果类型,如果能够避免它。

您只需编写一次这些结构,但是您会多次创建它们,因此您仍然希望进行简洁的创建(您会得到)。静态类型可确保您从中获取的数据符合您的期望。

现在,尽管如此,仍然有一些地方可以让简单的元组或列表有意义。返回数组的内容可能会减少开销,如果是这种情况(并且开销很大,您可以通过分析确定),则在内部使用简单的值数组可能很有意义。您的公共API可能仍应具有明确定义的类型。


最后,我使用了自己的枚举而不是TimeUnit,因为我只是将UNDER1MS与实际时间单位一起添加了。所以现在我只有两个领域,而不是三个。(从理论上讲,我可能会滥用TimeUnit.NANOSECONDS,但这会非常令人困惑。)
Mikhail Ramendik

我没有创建一个吸气剂,但是现在我知道了,如果originalTimeUnit=DurationTimeUnit.UNDER1MS调用者尝试读取毫秒值,那么吸气剂将如何引发异常。
Mikhail Ramendik '17

我知道您已经提到了它,但是请记住,使用python元组的示例实际上是关于动态vs静态类型的元组的。关于元组本身并没有说太多。您可以拥有将无法编译的静态类型的元组,正如我确定您知道的那样:)
Andres F.

1
@Veedrac我不确定你的意思。我的观点是,可以像这样更详细地编写库代码(即使花费更长的时间),因为使用代码的时间比编写代码要花费更多的时间。
约书亚·泰勒

1
@Veedrac是的,是的。但我坚持认为,有意义地重用的代码与不会有意义地重用的代码之间有区别的。例如,在Java包中,某些类是公共的,并且可以从其他位置使用。这些API应该经过深思熟虑。在内部,有些类可能只需要互相交谈。这些内部耦合是快速而肮脏的结构(例如,预期具有三个元素的Object [])可能适合的地方。对于公共API,通常应该选择更有意义和更明确的内容。
约书亚·泰勒

7

问题是您将苹果与桔子相提并论。您询问了如何模拟返回多个值,而不是给出一个带有无类型元组的快速脏python示例的示例,实际上您收到了实际上的一线回答

接受的答案提供了正确的业务解决方案。无需使用临时的快速变通方法,您第一次需要抛弃并正确实现时就需要对返回值进行任何实际操作,而是一个POJO类,它与大量库兼容,包括持久性,序列化/反序列化,仪器和任何可能的东西。

这一点也不长。您唯一需要编写的就是字段定义。可以生成setter,getter,hashCode和equals。因此,您的实际问题应该是:为什么不会自动生成getter和setter而是一个语法问题(有人会说是语法糖问题)而不是文化问题。

最后,您想过要加速一点根本不重要的事情。与维护和调试系统所花费的时间相比,编写DTO类所花费的时间微不足道。因此,没有人会优化以减少冗长。


2
“没人”在事实上是不正确的-有大量的工具可以优化以减少冗长,正则表达式是一个极端的例子。但是您可能的意思是“主流Java世界中没有人”,因此阅读答案很有意义,谢谢。我自己对冗长的关注不是花时间写代码,而是花在阅读代码上。IDE不会节省阅读时间;但似乎是这样的想法,即99%的情况是仅读取接口定义,因此应该简洁吗?
Mikhail Ramendik '17

5

这是有助于您观察的三个不同因素。

元组与命名字段

也许是最琐碎的-在其他语言中,您使用了元组。讨论元组是否是一个好主意并不是真正的重点-但是在Java中,您确实使用了较重的结构,因此这是一个有点不公平的比较:您可以使用对象数组和某种类型转换。

语言语法

声明这个类会更容易吗?我并不是说要公开字段或使用地图,而是像Scala的案例类那样,它提供了您描述的设置的所有好处,但更加简洁:

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

我们可以做到这一点-但要付出代价:语法变得更加复杂。当然,在某些情况下,甚至在大多数情况下,甚至在未来5年内的大多数情况下,都值得这样做-但需要进行判断。顺便说一句,这是您可以自行修改的语言(例如lisp)的优点-并且请注意由于语法的简单性,这如何成为可能。即使您实际上并没有修改语言,简单的语法也可以启用更强大的工具。例如,很多时候我都错过了Java可用的一些重构选项,但是Scala却没有。

语言哲学

但是最重​​要的因素是语言应该支持某种思维方式。有时可能会让人感到压抑(我经常希望支持某些功能),但是删除功能与拥有它们一样重要。你能支持一切吗?当然可以,但是您最好编写一个编译每种语言的编译器。换句话说,您将没有语言-您将拥有语言的超集,并且每个项目基本上都会采用一个子集。

当然,编写与语言哲学背道而驰的代码也是可能的,而且正如您所观察到的那样,结果通常很难看。在Java中仅具有几个字段的类类似于在Scala中使用var,将prolog谓词退化为函数,在haskell中执行unsafePerformIO等。Java类并不是轻巧的-它们不在那里传递数据。当某些事情看起来很困难时,退后一步,看看是否还有另一种方法通常是富有成果的。在您的示例中:

为什么持续时间与单位分开?有很多时间库可让您声明一个持续时间-诸如Duration(5,seconds)(语法会有所不同),然后它们将使您以更强大的方式执行所需的任何操作。也许您想将其转换-为什么要检查result [1](或[2]?)是否为“小时”并乘以3600?对于第三个论点-它的目的是什么?我猜想在某个时候您将不得不打印“少于1毫秒”或实际时间-这是时间数据所固有的一些逻辑。即您应该有一个这样的课程:

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

}

或您实际上想要对数据做的任何事情,从而封装了逻辑。

当然,在某些情况下,这种方式可能行不通-我并不是说这是将元组结果转换为惯用的Java代码的神奇算法!在某些情况下,它很难看而且很糟糕,也许您应该使用其他语言-这就是为什么毕竟有那么多的原因!

但是我对为什么类在Java中是“重型结构”的观点是,您不是要把它们用作数据容器,而是要用作自包含的逻辑单元。


我想要做的与持续时间实际上是有关使它们的总和,然后比较到另一个。不涉及任何输出。比较需要考虑四舍五入,因此我需要知道比较时的原始时间单位。
米哈伊尔·拉曼迪克

可能可以创建一种添加和比较我的类的实例的方法,但这可能会变得非常繁重。特别是因为我还需要除以浮点数并乘以浮点数(在该UI中有要验证的百分比)。所以现在我用final字段制作了一个不变的类,这看起来是一个不错的折衷方案。
Mikhail Ramendik '17

关于这个特定问题,最重要的部分是事物的一般性,从所有答案看来,情况似乎是一致的。
Mikhail Ramendik '17

@MikhailRamendik也许可以选择使用某种累加器-但您更清楚问题所在。的确,这并不是要解决这个特定的问题,对不起,如果我有些偏颇,我的主要观点是该语言不鼓励使用“简单”数据。有时,这可以帮助你重新审视自己的方法-其他时候一个int仅仅是一个int
萨诺斯Tintinidis

@MikhailRamendik关于样板方式的好处是,一旦有了一个类,就可以向其中添加行为。和/或使其与您一样不可变(多数情况下,这是一个好主意;您始终可以返回一个新对象作为总和)。最后,您的“多余”类可以封装您需要的所有行为,然后使吸气剂的成本微不足道。此外,您可能需要也可能不需要。考虑使用龙目岛autovalue
maaartinus 17-4-28的

5

据我了解,核心原因是

  1. 接口是抽象类的基本Java方法。
  2. Java只能从方法中返回一个值-对象或数组或本机值(int / long / double / float / boolean)。
  3. 接口不能包含字段,只能包含方法。如果要访问字段,则必须通过一种方法-因此是getter和setter。
  4. 如果方法返回接口,则必须具有一个实现类才能实际返回。

这使您“必须编写一个类以返回任何非平凡的结果”,这反过来又很沉重。如果您使用类而不是接口,则可以只具有字段并直接使用它们,但这将您绑定到特定的实现。


要添加到这一点:如果需要这在一个小范围内(比如说,在一个类中的私有方法),它的优良使用裸纪录-最好是不可改变的。但这真的真的不应该泄漏到班级的公共合同中(或更糟的是,图书馆!)。当然,您可以针对记录做出决定,以确保这种情况永远不会随着将来的代码更改而发生。这通常是更简单的解决方案。
a安

我同意“ Tuples在Java中不能很好地工作”的观点。如果您使用泛型,这将很快导致讨厌。
托尔比约恩Ravn的安徒生

@ThorbjørnRavnAndersen它们在Scala中工作得很好,Scala是具有泛型元组的静态类型语言。那么也许这是Java的错吗?
安德列斯F.

@AndresF。请注意“如果您使用泛型”部分。与Haskell相比,Java这样做的方式不会使其适用于复杂的结构。我不太了解Scala进行比较,但是Scala是否允许多个返回值是真的吗?这可能会很有帮助。
托尔比约恩Ravn的安徒生

@ThorbjørnRavnAndersenScala函数具有单个返回值,该返回值可能是元组类型(例如,def f(...): (Int, Int)是一个f返回恰好是整数元组的值的函数)。我不确定Java中的泛型是否是问题所在;请注意,例如Haskell也会键入擦除。我认为Java没有元组是没有技术原因的。
Andres F.

3

我同意JacquesB的回答

Java(传统上)是基于单范例类的OO语言,它针对显式性和一致性进行了优化

但是明确性和一致性不是要优化的最终目标。当您说“针对可读性优化了python”时,您立即提到最终目标是“可维护性”和“开发速度”。

当您具有显式性和一致性时,以Java方式完成了什么?我的看法是,它已经发展成为一种语言,声称可以提供可预测的,一致的,统一的方式来解决任何软件问题。

换句话说,对Java文化进行了优化,以使管理人员相信他们了解软件开发。

或者,就像一个很聪明的人很久以前说的那样

判断语言的最佳方法是查看其支持者编写的代码。“ Radix enim omnium malorum est cupiditas”-Java显然是面向金钱的编程(MOP)的示例。正如SGI的Java首席支持者告诉我的那样:“亚历克斯,你必须去赚钱的地方。” 但是我不是特别想去哪里有钱-通常在那儿闻起来不太香。


3

(此答案不是特别针对Java的解释,而是解决了“(繁重的)实践可以优化什么?”这一一般性问题。)

考虑以下两个原则:

  1. 当您的程序做正确的事情时,这很好。我们应该使编写正确的程序变得容易。
  2. 当程序执行错误的操作时,这很不好。我们应该使编写做错事情的程序更加困难。

试图优化其中一个目标有时可能会妨碍另一个目标(即,使做错事情变得更加困难,也可能使做正确的事情变得更加困难,反之亦然)。

在任何特定情况下进行哪种权衡取决于应用程序,所讨论的程序员或团队的决定以及(组织或语言社区的)文化。

例如,如果程序中的错误或几个小时的中断可能导致人员伤亡(医疗系统,航空航天)或什至只是金钱(如Google广告系统中的数百万美元),那么您将做出不同的权衡(不是不仅使用您的语言,而且还涉及工程文化的其他方面),而不是使用一次性脚本:它可能倾向于“繁重”的一面。

其他可能使您的系统更“沉重”的示例:

  • 当您有许多团队多年来从事大型代码库开发时,一个大问题就是某人可能会错误地使用别人的API。用错误的顺序调用参数的函数,或者在未确保其期望的前提条件/约束的情况下调用的函数,可能会造成灾难性的后果。
  • 作为一个特殊的例子,假设您的团队维护一个特定的API或库,并希望对其进行更改或重构。用户对代码使用方式的“约束”越多,更改代码就越容易。(请注意,这里最好有实际的保证,那就是没有人会以异常的方式使用它。)
  • 如果开发是由多个人或团队分散进行的,那么让一个人或团队“指定”接口并让其他人实际实现它似乎是一个好主意。为了使它起作用,您需要能够在实现完成时获得一定程度的信心,即实现实际上与规范匹配。

这些只是一些示例,旨在让您了解使事情“繁重”(并且使您更难于快速地编写一些代码)确实是故意的。(甚至可能会争辩说,如果编写代码需要付出很多努力,这可能会使您在编写代码之前进行更仔细的考虑!当然,这种说法很快就会变得荒谬。)

例如:Google的内部Python系统往往使事情变得“繁重”,以致您不能简单地导入其他人的代码,而必须在BUILD文件中声明依赖项,要导入其代码的团队需要将其库声明为对您的代码可见,等等。


注意:以上所有都是关于事情何时变得“沉重”的。我绝对声称Java或Python(语言本身或它们的文化)在任何特定情况下都具有最佳的权衡;这是您要考虑的。关于这种权衡的两个相关链接:


1
怎么样阅读的代码?那里还有另一个权衡。用怪异的咒语和丰富的仪式压制的代码更难读;在您的IDE中使用样板文件和不必要的文本(以及类层次结构!),将很难看到代码的实际作用。我可以看到这样的论点,即代码不一定必须易于编写(不确定我是否同意它,但是它有一些优点),但绝对应该易于阅读。显然可以并且已经使用Java构建了复杂的代码-否则声明是愚蠢的-但这是由于它的冗长还是尽管如此
Andres F.

@AndresF。我对答案进行了编辑,以使其更清楚地表明它不是专门针对Java的(我不是超级粉丝)。但是,是的,在阅读代码时需要权衡:一方面,您希望能够轻松阅读您所说的“代码实际在做什么”。另一方面,您希望能够轻松查看以下问题的答案:该代码与其他代码有何关系?该代码可以做的最坏的事情:对其他状态的影响可能是什么,其副作用是什么?该代码可能取决于哪些外部因素?(当然,理想情况下,我们希望对这两组问题都能够提供快速解答。)
ShreevatsaR

2

Java文化已经随着时间的流逝而发展,它受到来自开放源代码和企业软件背景的重大影响-如果您真的考虑过,这是一个奇怪的组合。企业解决方案需要重型工具,而开源则要求简单。最终结果是Java位于中间位置。

影响该建议的部分原因是,在Python和Java中,可读性和可维护性是非常不同的。

  • 在Python中,元组是一种语言功能。
  • 在Java和C#中,元组都是(或将是)一个功能。

我仅提及C#,因为标准库具有一组Tuple <A,B,C,.. n>类,并且如果该语言不直接支持它们,则这是一个很好的例子,说明了元组多么笨拙。在几乎每种情况下,如果您选择了很好的类来处理问题,您的代码就会变得更具可读性和可维护性。在链接的Stack Overflow问题中的特定示例中,其他值将很容易表示为返回对象上的已计算getter。

C#平台所做的一个有趣的解决方案提供了一个令人满意的中间立场,那就是匿名对象(在C#3.0中发布)的想法,这种想法非常容易解决。不幸的是,Java还没有类似的东西。

在修改Java语言功能之前,最易读和可维护的解决方案是拥有一个专用对象。这是由于该语言的限制可以追溯到1995年开始。原始作者计划了许多语言功能,而这些功能从未实现过,而向后兼容性则是Java随时间演变的主要限制之一。


4
在最新版本的c#中,新的元组是一等公民,而不是图书馆类。到那里花了太多版本。
伊戈尔·索洛伊登科

最后三段可以概括为“语言X具有您所寻求的Java所没有的功能”,我看不到它们为答案提供了什么。为什么在Python to Java主题中提到C#?不,我只是不明白要点。一个20k +代表家伙有点怪异。
奥利维尔·格雷戈尔

1
正如我所说,“我只提到C#是因为Tuples是一种库功能”,类似于Java在主库中包含Tuples的情况下的工作方式。使用起来非常笨拙,更好的答案是总是有一个专门的课程。不过,匿名对象确实会给元组设计一个类似的难题。没有更好的语言支持,您就必须使用最可维护的工具。
Berin Loritsch

@IgorSoloydenko,也许对Java 9有希望?这只是lambda的逻辑下一步。
Berin Loritsch

1
@IgorSoloydenko我不确定这是不是真的(一等公民)-它们似乎是语法扩展,用于从库中创建和解散类的实例,而不是像类,结构,枚举一样在CLR中具有一类支持。
Pete Kirkham

0

我认为在这种情况下使用类的核心问题之一是,在一起的东西应该保持在一起。

我已经对方法参数进行了另一种讨论:考虑一个简单的方法来计算BMI:

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

在这种情况下,我会反对这种风格,因为体重和身高是相关的。当它们不是时,该方法“传达”这是两个单独的值。您何时会用一个人的体重和另一个人的身高来计算BMI?这是没有道理的。

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

更加有意义,因为现在您可以清楚地说明身高和体重来自同一来源。

返回多个值也是如此。如果它们明确连接,则返回整洁的小包装并使用对象(如果它们未返回多个值)。


4
好的,但是假设您接下来有一个(假设的)任务来显示图表:BMI如何在某些特定的固定高度上取决于体重。然后,如果不为每个数据点创建一个Person,就无法做到这一点。而且,将其发挥到极致,如果没有有效的出生日期和护照号码,就无法创建Person类的实例。怎么办?顺便说一句,我没有拒绝投票,在某些类中将属性捆绑在一起是有充分理由的。
artem

1
@artem是接口起作用的下一步。因此,personweightheight界面可能是这样的。您在我所举的例子中陷入了困境,即指出一切应该在一起。
Pieter B

0

坦率地说,这种文化是Java程序员最初倾向于来自大学,那里教授面向对象的原理和可持续的软件设计原理。

正如ErikE在回答中用更多的话说的那样,您似乎并没有编写可持续的代码。我从您的示例中看到的是,存在一个非常尴尬的问题缠结。

在Java文化中,您将倾向于了解可用的库,这将使您获得比现成的编程更多的功能。因此,您将需要针对在硬核工业环境中尝试过并经过测试的设计模式和样式进行特殊处理。

但是正如您所说,这并非没有缺点:今天,使用Java已有10多年的时间,我倾向于使用Node / Javascript或Go进行新项目,因为两者都可以加快开发速度,而微服务风格的体系结构则可以通常就足够了。从Google最初大量使用Java的事实来看,但它一直是Go的创始者,我想他们可能也在这么做。但是,即使我现在使用Go和Javascript,我仍然使用多年使用和理解Java所获得的许多设计技能。


1
值得注意的是,谷歌使用的foobar招聘工具允许将两种语言用于解决方案:java和python。我发现Go不是一种选择,这很有趣。我偶然发现了Go上的这个演示文稿,其中有关于Java和Python(以及其他语言)的一些有趣观点。例如,有一个突出的要点:“也许,因此,非专家程序员混淆了“搭配解释和动态类型一起使用。” 值得一读。
JimmyJames

@JimmyJames刚到幻灯片上说:“在专家的手中,他们很棒” –问题中的问题的好摘要
Tom
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.