我在一次采访中,那个家伙让我做一个典型的FizzBu​​zz问题,打印数字1-100,但是对于3个打印Fizz的每个因子,5个打印Buzz的每个因子以及两个FizzBu​​zz的每个因子。我写了以下内容:

public static void fizzbuzz(){
    for(int i=1;i<=100;i++){
        if(i%15 == 0){
            System.out.println("FizzBuzz");
        } else if (i%3 == 0) {
            System.out.println("Fizz");
        } else if (i%5 == 0) {
            System.out.println("Buzz");
        } else {
            System.out.println(i);
        }
    }
}


面试官让我更改了答案,使它看起来像这样:

public static void fizzbuzz(){
    for(int i=1;i<=100;i++){
        String str = "";
        if(i%3 == 0){
            str = "Fizz";
        }
        if(i%5 == 0){
            str += "Buzz";
        }
        if(str.equals("")){
            str = i;
        }
        System.out.println(str);
    }
}


然后他问我,如果我把那个代码交给我,我会更愿意,并说没有错误的答案(我应该知道这是个谎言),但是我愚蠢的说我的,因为这对我来说更容易看和看。只是浏览一下,发现代码中的15因子发生了一些变化,而在我脑海中没有真正遍历代码的情况下,我对第二个版本一无所知。 ,我的问题是,由于我遗失的某些原因,他的版本更好吗?我具有入门级技能,因此老实说,这两个版本在效率和可读性方面都是完全相同的。

评论

他可能一直在撒谎,但是当被问到这个问题时,答案并没有错。辩论的双方都有人。我有一种感觉,他正试图了解您的思考过程。

进行FizzBu​​zz深入分析的访问者有点缺少FizzBu​​zz的要点-过滤掉无法写东西的候选人。

@Justin可能的问题是,筛选出记住了一个正确的FizzBu​​zz实现的候选人

#1 楼

我喜欢他的解决方案。我的理由是:


您只能在字符串操作中使用常量
您不进行字符串连接
每个其余部分的逻辑都是不同的。

这两个选项可能会更好。缺少“喘息空间”(关键字,变量,值和运算符之间的空白)会导致代码难以阅读。像这样的行:


for(int i=1;i<=100;i++){



应为:

for (int i = 1; i <= 100; i++) {


然后,作为switch语句,您的解决方案实际上会更有效:不是if/elseif/elsif/else的概念...仅是一项条件检查。

评论


\ $ \ begingroup \ $
我也认为应该避免字符串串联,但这似乎是一场无休止的辩论。无论哪种方式,您都可以在此处实现有趣的实现。
\ $ \ endgroup \ $
–RubberDuck
2014年8月15日15:59

\ $ \ begingroup \ $
非常有趣的解决方案,性能可能更高,但我认为书包更难阅读和快速理解
\ $ \ endgroup \ $
–user3791372
2014年8月15日在20:53

\ $ \ begingroup \ $
太酷了。我终于明白在大多数情况下需要使用break关键字的意义
\ $ \ endgroup \ $
–电话
14年8月17日在18:23

#2 楼


然后他问我,如果我将代码交给我,我会更愿意


我要说的是:这取决于。

rolfl已经添加了有关您的解决方案的一些注释,因此我将添加有关第二个解决方案的一些注释(优点和缺点)。

首先,rolfl关于间距的注释当然也适用于此。

他的解决方案的最大优点是它更灵活/可扩展。如果您想添加Woof,Ping,Plop,则无需编写大量代码即可轻松实现。

但是,缺点是:


它使用String(而不是StringBuilder)创建了更多对象。
由于str = i;是非法分配(不能将字符串设置为int),因此无法编译。您必须使用str = String.valueOf(i);Integer.toString(i);

使用if (str.isEmpty())而不是if (str.equals(""))

要好得多,而不是将字符串设置为整数(如果为空),请使用System.out.println(i);获得一些速度。 (因为您不必为整数创建字符串)

最后,这完全取决于您想要的内容。如果您想权衡速度以获得灵活性,那么第二个版本会更好(一旦您纠正了以上几点)。使用开关,即使这会稍微降低代码的可维护性。)

并非全都是黑白,这里有很多优点和缺点。当我被问到这个问题时,我会提出这些利弊,然后问面试官:“我们以后再需要添加Woof,Ping,Plop的可能性是多少?” (注意:我在采访中还没有这样回答)

评论


\ $ \ begingroup \ $
仅当您假设“ fizzbuzz”是“ fizz”和“ buzz”的组合时,审阅者解决方案才更加灵活。如果必须在15的倍数上打印“ fozzbozz”怎么办?
\ $ \ endgroup \ $
– ThatOneGuy
14年8月15日在16:48

\ $ \ begingroup \ $
@ user1895420是的,但是到目前为止,我还没有看到这样的作业。我认为该分配更有可能为另一个数字添加“ Woof”。
\ $ \ endgroup \ $
–西蒙·福斯伯格
14年8月15日在16:54

\ $ \ begingroup \ $
“它不能编译为str = i;是非法分配”,而缺少stringbuilder是我最大的wtf!
\ $ \ endgroup \ $
–user3791372
2014年8月15日20:52

\ $ \ begingroup \ $
无论上下文如何,我都不会在面试官的脸上碰到不编译的内容。 “很高兴您认为可以以不同的方式进行操作,但是此代码可以工作。无故更改它是一个坏主意……我的意思是,您的代码甚至没有编译,为什么要这样更改它?”然后翻转问题。
\ $ \ endgroup \ $
– Pimgd
2014年8月15日23:52

\ $ \ begingroup \ $
我相信String vs StringBuilder实际上是无关紧要的。编译器会将String串联转换为使用StringBuilder。只有当变量在循环外部声明时,它才不能替换为StringBuilder,这会导致额外的开销。即:此SO问题/答案。
\ $ \ endgroup \ $
–冷冻豌豆的传承
2014年8月17日下午5:33



#3 楼

这是对第二个的回顾-我认为第一个完全可以接受,第二个我遇到了问题。

字符串

里面有一个字符串最后打印出来。好。但是,每当整数可被5整除时,就会通过以下指令更改字符串:如果它是可变的,请改为使用StringBuilder。

我知道人们会去“但这只会击中20次-20个新对象有什么大不了?”答案是“不,没有20个新的objets没什么大不了的-但这表明有人会将其放入一个循环中,该循环将被执行数千次,并想知道内存的去向”(我已经确定了该代码之前的代码)。

我们正在使用str += "Buzz";对字符串进行测试。由于String的插入,因此此+=与循环开始时的.equals("")完全相同。

这意味着可以按以下方式进行测试:

    if(str.equals("")){
        str = i;
    }


我现在声明免责声明,尽管这是正确的并且会更快,但是静态分析工具会抱怨它,而查看它的人会认为这是一个错误。您可能不应该那样做...在String类的.equals(Object anObject)的第一个测试中,它无论如何都要进行该测试。

如果String不是"",则是其中之一,那么在返回false之前还需要进行更多的测试。

测试以下内容:

    if(str == ""){
        str = i;
    }




    if(str.length() == 0){
        str = i;
    }


是一个更好,更简洁的测试,它无需遍历""中的所有代码即可准确说明其含义。

    if(str.isEmpty()){
        str = i;
    }


我说的是正确的,因为对于此代码,它始终会给出正确的答案。这不是好的代码。出于充分的原因,这将导致静态分析工具对此进行标记。这将使您的同事查看它,并认为您那里有bug-有充分的理由。这不是好代码。

用Java编写""过多。 IntelliJ在“性能问题”下进行的一项静态分析检查是.equals()


报告.equals()被调用以将String与空字符串进行比较。通常,通过将.equals("")与零进行比较来测试String是否为空,这会更有效。


因此,应考虑的选项包括:

    if(str == ""){
        str = i;
    }




    if(str.length() == 0){
        str = i;
    }


(尽管一天结束时我会选择String.equals("")代替.length()) 。

那么,为什么这是一个问题?让我们看一下.equals(Object anObject)的代码(在6-b14中,该代码与从7u40-b43到8-b132稍有不同,尽管本质上是相同的。)。 >
如果它们是同一对象(对于因为isEmpty()length() == 0都不是""而掉落的情况,这将是正确的,它将直接从前面踢出去,我们就完成了。但是,如果字符串为i%3i%5"Fizz",然后将检查以确保其为String的实例,然后测试计数(在构造函数中设置的私有最终值)。

"Buzz""FizzBuzz"为0 ,而其他字符串是4或8,因此我们将在1031处返回false。

那么为什么大不了?

好,count的代码是:

    if(str.isEmpty()){
        str = i;
    }


确实简短。方法调用的简短性很重要。这些没有人碰过的""调用之一(他们也不应该在不知道自己在做什么的情况下)是:isEmpty()(Java HotSpot VM选项)。默认值是35个字节的字节码。

如果您使用的是小方法,HotSpot将内联该方法。在HotSpot决定要执行的操作之后,没有堆栈框架,也没有方法查找。恰好在代码中。

1012    public boolean equals(Object anObject) {
1013        if (this == anObject) {
1014            return true;
1015        }
1016        if (anObject instanceof String) {
1017            String anotherString = (String)anObject;
1018            int n = count;
1019            if (n == anotherString.count) {
1020                char v1[] = value;
1021                char v2[] = anotherString.value;
1022                int i = offset;
1023                int j = anotherString.offset;
1024                while (n-- != 0) {
1025                    if (v1[i++] != v2[j++])
1026                        return false;
1027                }
1028                return true;
1029            }
1030        }
1031        return false;
1032    }


看起来更像
这就是为什么人们说Java中的许多小方法是一件好事-因为编译器会内联它们。

所以,这是最好的做法,因为它确切地显示了您关心的内容阅读人员的代码不会因调用大型方法而陷入困境,这种方法在这种情况下会执行许多您不关心的事情。

670     public boolean isEmpty() {
671         return count == 0;
672     }


字符串是否为空? ...我们就完成了。意图很明确,代码也更简单。

如果以前是为此目的而切换到StringBuilder,则没有-XX调用,但是有一个-XX:MaxInlineSize=#调用返回了计数应该使用(AbstractStringBuilder 6-14中的源代码)。


StringBuilder.append与isEmpty()带字符串的字节码怎么样? length()更容易阅读,它的处理方式是什么?

让我们花一些快速代码...

    if(str.isEmpty()){
        str = i;
    }


看看

    if(str.count == 0){
        str = i;
    }


这是什么意思?

第38行(+=行):


创建一个新的StringBuilder
在其上附加+=

调用Integer.toString将整数转换为String
在上一步中使用String调用字符串生成器上的append
将StringBuilder转换回String
将其存储在foo

中,请注意,创建的StringBuilder现在可以(连同已创建的允许+=工作的String一起处理,因为定义了String + = String,但未定义String + = int。

另一方面,第39行则: >

调用StringBuilder.append并添加一个整数。

另一个选择String.concat。


调用Integer.toString将整数转换为String
调用String.concat(创建新的String对象)
将新字符串存储在qux中并且针对将整数放入数组进行了优化。其中对foo的调用与Integer.toString的代码相同。

关键是没有额外的String为此创建(并丢弃) e Integer,当您初次使用StringBuilder时,也不会为+=创建(或丢弃)StringBuilder。我感到有些惊讶,因为单个右侧的代码不仅仅会执行String.concat(String)调用,这可能会更好(尽管仍然更少,但您仍然可以创建一堆Strings) 。但是,此优化将需要额外的工作,因为如果该范围内的某个地方存在另一个Integer.getChars或一系列+=,则可能会使用与最初分配的相同的StringBuilder。

不过,重点仍然是-如果您要对String进行变异,请从可变的String类开始并进行处理。有多种方法可以编写比+=更多的预格式化代码,并且考虑到如何使用在循环内具有String foo = bar + qux + baz的String来避免不必要的String对象浮出水面。在某些情况下,当您只做一个右手侧而不是重复做一次时,+=可能会很好(FizzBu​​zz代码可能适合这样做),但是StringBuilder仍然是一个更好的选择。

评论


\ $ \ begingroup \ $
Urgh。依靠字符串实习获得假定的性能优势而无需实际衡量?我说那是迄今为止您所能做的最糟糕的事情。等同的第一件事是身份比较,因此,性能好处归结为可能略微减小了代码大小(我挑战任何人来衡量差异)。 isEmpty是编写此代码的最干净的方法。
\ $ \ endgroup \ $
– Voo
2014年8月15日在18:17



\ $ \ begingroup \ $
@Voo不是性能问题,出于多种原因它是错误的...是的,已经知道了-“并且它将在.equals()的第一个测试中进行测试String类中的对象anObject)。” -但如果==的性能不尽相同,当有其他方法可以更有效地工作并传达意图时,它需要做的其他工作才能设置为不同的==一样。
\ $ \ endgroup \ $
–user22048
2014年8月15日在18:20

\ $ \ begingroup \ $
公平的观点,但这仍然只是比较两个内部数组的长度-即使对于所做的少量工作,我也怀疑这是可以测量的。但是无论如何,使用isEmpty都应该和身份比较一样有效,并且不会依赖于字符串插入。
\ $ \ endgroup \ $
– Voo
14年8月15日在18:24

\ $ \ begingroup \ $
因为String是不可变的,所以length是私有的final int。方法的长度为isEmpty和length都使HotSpot可以快速内联它们。值的计算是在String的构造函数中完成的,与引用vs引用比较相比,您有一个int vs int比较。请注意,这在经过类型检查行1019之后也相等,但是如果可以使用isEmpty使测试清楚,则效果会更好。
\ $ \ endgroup \ $
–user22048
14年8月15日在18:30

\ $ \ begingroup \ $
重新。 StringBuilder,首先您确定+ =没有优化以避免重新分配吗?其次,那不是矫kill过正吗?这在某种程度上听起来对我来说太过分了。
\ $ \ endgroup \ $
–djechlin
14年8月16日在18:12

#4 楼

它们每个都有问题,但是我认为第一个存在问题是可以接受的。

第一个具有的缺陷是,它需要唯一的素因式分解和a posteriori的事实。这是一个众所周知的事实,但问题是“如果3则嘶嘶声,如果5则嗡嗡声,如果两者都嘶嘶声”。问题是“如果两者都存在”。所以写“ both”,意思是“ 3*5=15”。当原始问题没有问题时,您使用了数学技巧来压缩该表达式,因此您对原始问题失去了忠诚。实际上,数学技巧的简单性还不止于此。

第二个可怕,除了我可以想到其中的一种优势。

逐字符字符串操作引导每个角色的思考。为什么将我拖入这种分析?键入和思考“ FizzBu​​zz”要比思考“ FizzBu​​zz”是多么漂亮的东西(由它的组成部分组成)要容易得多。

字符串容易使人烦恼。即,新生产线现在去哪里了?它可以正常工作,但我保证您需要编写或评估它。

第二个程序可扩展。鉴于问题2的工作,FizzBu​​zzBazz会是编写起来容易得多的程序。它不是完全由数据驱动的,但接受与素数3,5,7,11 ...相对应的字符串数组非常容易,然后转向第二个程序进入循环。

还要注意,在更一般的情况下,问题陈述需要考虑字符串连接。总的来说,我宽恕了数据驱动的概括,因此,如果程序的输入是素数到字符串的映射,并且是用循环而不是多余的if块实现的,那么我可能会称其为聪明,但最好方式-它解决了一个更普遍的问题,并且数据驱动了幻数,因此具有更好的逻辑与外观分离。

对于踢球者,还请注意,第一步是远离泛化的步骤。 。对于它的价值,我希望两者中的第一个。

评论


\ $ \ begingroup \ $
“字符串很容易使人烦恼。也就是说,新行现在在哪里?” - 你在说什么??
\ $ \ endgroup \ $
–西蒙·福斯伯格
14年8月15日在16:43

\ $ \ begingroup \ $
@SimonAndréForsberg作为C程序员有太多经验。但是,存在直观上等效的但不正确的方式来构建字符串。例如尽早输出if(i%3 == 0)println(“ Fizz”); if(i%5 == 0)println(“ Buzz”)已被窃听。程序2具有一个“一次性输出”规则,可以直接处理该规则。
\ $ \ endgroup \ $
–djechlin
2014年8月15日在16:55



\ $ \ begingroup \ $
我会说两个版本都只输出一次,因为第一个版本使用if-elseif链明确分隔。当开始时,新线问题不是很相关。
\ $ \ endgroup \ $
–西蒙·福斯伯格
2014年8月15日在17:11

\ $ \ begingroup \ $
@SimonAndréForsberg我想指出一点,没有字符串操作比字符串操作更好。在等效的C程序中,仅在第二个版本中,此问题才变得可怕。
\ $ \ endgroup \ $
–djechlin
14年8月15日在18:07

\ $ \ begingroup \ $
+1表示“ 3 * 5 = 15”。这是一个众所周知的事实。甚至量子计算机都知道ia.ucsb.edu/pa/display.aspx?pkey=2803
\ $ \ endgroup \ $
–基思
2014年8月18日,下午3:45

#5 楼

您的技术的可扩展性/可维护性较差

由于没有人提到可扩展性,我认为值得对您的编码技术进行比较。让我们想象一下,今天的编码要求已从众所周知的FizzBuzz更改为涉及FizzBuzzCazz倍数的新7

您的扩展代码:

public static void fizzbuzz(){
    for(int i=1;i<=100;i++){
        //Luckily we can skip i =3*5*7 = 105
        if(i%35 == 0) {
            System.out.println("BuzzCazz");
        } else if (i%21 == 0) {
            System.out.println("FizzCazz");
        } else if (i%15 == 0){
            System.out.println("FizzBuzz");
        } else if (i%7 == 0) {
            System.out.println("Cazz");
        } else if (i%5 == 0) {
            System.out.println("Buzz");
        } else if (i%3 == 0) {
            System.out.println("Fizz");
        } else {
            System.out.println(i);
        }
    }
}


面试官的扩展代码:

public static void fizzbuzz(){
    for(int i=1;i<=100;i++){
        String str = "";
        if(i%3 == 0){
            str = "Fizz";
        }
        if(i%5 == 0){
            str += "Buzz";
        }
        if(i%7 == 0){
            str += "Cazz";
        }
        if(str.equals("")){
            str = i;
        }
        System.out.println(str);
    }
}


看看面试官的代码可扩展得多?

#6 楼

尽管第二种实现并不十分漂亮,但第一种却使我感到不安,因为它涉及冗余。冗余使代码更难维护,并且在必须维护时更容易出现错误。

例如,明天,您将收到一系列新要求:


在原本要打印“ Fizz”的地方打印“ Feez”,然后按8的因子进行打印,而不是3
对8的每个因子进行“嗡嗡声”打印7和5的每个因数
与原始要求保持一致,请在适用的情况下都打印“ FeezBuzz”。

要求1-3中的每一个都需要在一个地方修改示例2,而它们又需要分别在两个地方修改示例1(总共六个,而#3对于示例1来说特别棘手)。

需求4完全不需要对示例2进行任何更改,但是需要对示例1进行修改。

如果您在一个地方实现了新的需求之一,但是忘记了另一个,您的代码是错误的。这是因为示例1违反了DRY原则,我认为这是编写质量代码最重要的关键之一。

#7 楼

我更喜欢第一个版本,因为代码的结构更直接。

第二个版本可能具有明显的优势,因为每个字符串“ Fizz”和“ Buzz”仅包含一次,但是

我认为通过显式设置%3%5检查可以使您的版本更具可读性:

public static void fizzbuzz(){
    for(int i=1; i<=100; i++){
        if(i%3 == 0){
            if (i%5 == 0) {
                System.out.println("FizzBuzz");
            } else {
                System.out.println("Fizz");
            }
        } else {
            if (i%5 == 0) {
                System.out.println("Buzz");
            } else {
                System.out.println(i);
            }
        }
    }
}


如果逻辑是这样的话,则可以更容易地看到所陈述的规则和代码之间的联系。这也消除了规则中未说明的神奇常数15

#8 楼

两种代码都有其自身的优缺点。


速度-您的代码速度更快,因为它在串联String对象时没有开销处理。在Java中,String是不可变的。实际上,每次执行连接时,都会创建一个新的String对象。
可扩展性-您的代码扩展性较差。我同意@awashburn的回答。您的代码不够灵活,无法满足不断变化的需求。
可维护性-您的代码更具可维护性。您可以直接跟踪输出,因为在每种情况下,您都是静态指定目标输出。
可靠性-您的代码不太可靠。输出是通过组合诸如Fizz,Buzz等之类的词构成的。它们易于出现印刷错误(人为倾向)。
可重用性-两种代码最终都难以重用。该方法应接受输入并返回输出。另外,请勿将System.out.println放在其中。


评论


\ $ \ begingroup \ $
这对讨论来说是一个很好的总结,但是我想考虑一下在可维护性方面,第二个显然更好。但是,这与可扩展性有关。
\ $ \ endgroup \ $
–user248871
15年9月23日在18:06

#9 楼

就个人而言,我更喜欢您的代码。

速度差异几乎可以忽略不计;瓶颈是IO。因此,无需深入研究rolfl的答案。

对于标准FizzBu​​zz问题,几乎无需关注可扩展性。

最重要的是,一段代码可以更容易理解吗?您的代码明确声明了当一个数字是3的倍数,5的倍数或两者同时发生时的情况。另一方面,要找出第二段代码,需要进行一些跟踪。因此,我希望使用您的代码。

但是,如果FizzBu​​zz问题有可能扩展为涵盖大量数字,那么任何一种解决方案都无法彻底解决问题。应该改用一般的FizzBu​​zz。

#10 楼

对我而言,第二种解决方案存在严重的鲁棒性问题。前两个if语句很好(如果您不关心性能)。他们对输入进行测试,产生结果,并将其分配给输出变量str。但是第三个测试if (str.equals(""))是在输出变量而不是输入i上完成的。这是执行请求的行为的复杂方法,因此代码的可读性较低。这种方法在更复杂的问题中容易出错。

我认为,第一种解决方案的可扩展性比% 15差。大多数说第一种解决方案扩展性较差的评论忽略了需求可能发生变化的可能性,使得因子3和5都应打印“豪猪”,甚至绝对不打印。无论哪种情况,您的代码都将更易于更新。

我建议,如果您将第一个测试更改为if (i%3 == 0 && i%5 == 0)(尽管有额外的操作,但为了保持清晰度和可扩展性)并且使用常量来利用“ FizzBu​​zz”的优美之处,您的答案将是理想的是Fizz和Buzz的串联(这是第二个解决方案的唯一“优势”):

private static final String OUTPUT_A = "Fizz";
private static final String OUTPUT_B = "Buzz";
private static final String OUTPUT_AB = OUTPUT_A + OUTPUT_B;