我决定接受FizzBu​​zz挑战,我将使用Java 8概念使它有点模块化,但仍然让它成为一个简短,易读和易懂的程序。

我在网上发现的一些宝石:FizzBu​​zzEnterpriseEdition

问题描述:


编写一个程序,打印从1到100的数字。但是要是三个的倍数打印“ Fizz”而不是数字,并打印五个“ Buzz”的倍数。对于三个和五个的倍数的数字,打印“ FizzBu​​zz”


这是我的代码:

public class FizzBuzz {
    private static Stream<String> fizzBuzz(final int min, final int max) {
        if (min < 0) {
            throw new IllegalArgumentException("min is negative: min = " + min);
        }
        if (min > max) {
            throw new IllegalArgumentException("min > max: min = " + min + " / max = " + max);
        }
        return IntStream.rangeClosed(min, max)
                .mapToObj(FizzBuzz::fizzBuzzify);
    }

    private static String fizzBuzzify(final int value) {
        StringBuilder stringBuilder = new StringBuilder();
        boolean toDefault = true;
        if (value % 3 == 0) {
            stringBuilder.append("Fizz");
            toDefault = false;
        }
        if (value % 5 == 0) {
            stringBuilder.append("Buzz");
            toDefault = false;
        }
        return (toDefault) ? String.valueOf(value) : stringBuilder.toString();
    }

    public static void main(String[] args) {
        fizzBuzz(1, 100).forEach(System.out::println);
    }
}


我是仍然在寻找一种更好的方式编写fizzBuzzify,但是我的意图是不像if (value % 15 == 0)那样对if (value % 3 == 0 && value % 5 == 0)进行硬编码,因为它会产生一种不合逻辑的操作优先级,因为您绝对需要先编写if (value % 15 == 0)的情况,然后再写3-情况和5情况(反之亦然)。

评论

我认为,任何不使用简单的for循环和System.out.println的FizzBu​​zz解决方案都无法通过测试。

@RemcoGerlich FizzBu​​zz挑战中没有什么要求解决方案是我所必须的。您为什么要提出这一主张?

@Rune FS:这不是挑战,但是使程序复杂到需要的程度并不是很好的编程。

使用for循环的@REmcoGerlich比编写此纯函数要复杂得多。

@RuneFS因为您这么说?这就是Java,它中的任何功能都无法纯粹发挥作用,那么您的评论的重点是什么?

#1 楼

我用来决定是否使用的准则是,它取决于我是否事先知道将使用多少次。我相信我曾经在某些MSDN页面上遇到过此建议,但我不确定。

对于您来说,您知道StringBuilder仅有两种可能的用法,并且在循环内未添加任何值,因此我将使用普通的字符串连接:

现在您还可以将代码更改为我认为更易于解释的内容:

评论


\ $ \ begingroup \ $
result.length> 0很好,我喜欢!不再需要(愚蠢的)变量。
\ $ \ endgroup \ $
–skiwi
2014年7月12日12:20



\ $ \ begingroup \ $
比result.length> 0稍好于!result.isEmpty()
\ $ \ endgroup \ $
– janos
2014年7月12日在12:22

\ $ \ begingroup \ $
@janos:我不能说我特别喜欢它。从技术上讲,字符串是char的容器,但仍然感觉好像它主要是基本对象而不是数据结构。 .isEmpty()是我与集合关联的东西。
\ $ \ endgroup \ $
– Jeroen Vannevel
2014年7月12日在12:24

\ $ \ begingroup \ $
@JeroenVannevel在我脑海中的逻辑是“如果字符串为空,则执行此操作,否则执行该操作”,而不是“如果字符串的长度大于0”,因此以这种方式编写感觉很自然。顺便说一句,集合也有一个isEmpty方法,但这根本不会打扰我
\ $ \ endgroup \ $
– janos
2014年7月12日在12:31

\ $ \ begingroup \ $
@JeroenVannevel至少用英语来说,零长度的字符串称为空字符串,而isEmpty则很有意义。
\ $ \ endgroup \ $
– David Harkness
2014年7月12日19:18

#2 楼

我认为,如果您要这样做,最好将条件分开。我不太了解Java 8来使用它,但是在较旧的Java中,我会考虑使用类似的东西(请注意,这并不是旨在作为可编译Java,而只是类似Java的伪代码): br />
interface Substitute {
    Boolean condition(int);
    String transform(int);
}

class Fizzer : implements Substitute {
    Boolean condition(int x) { return x % 3 == 0; }
    String transform(int) { return "fizz"; }
};

class Buzzer : implements Substitute {
    Boolean condition(int x) { return x % 5 == 0; }
    String transform(int) { return "buzz"; }
};

class Mapper {
    List<Substitute> subs;

    void add_sub(Substitute sub) {
        subs.Add(sub);
    }    

    String execute(int input) { 
        String result;
        Boolean use_default = true;

        foreach (sub : subs) {
            if (sub.condition(input)) {
                result += sub.transform(input);
                use_default = false;
            }
        }
        if (use_default) return String.valueOf(input);
        return result;
    }
}

class FizzBuzz {
    static void main() { 
        Mapper map;
        map.add_sub(new Fizzer);
        map.add_sub(new Buzzer);

        for (int i=0; i<100; i++)
           System.out.println(map.execute(i));
    }
}


这从各个条件和产生的结果中分离出“如果满足某些条件,则用字符串代替数字”的基本概念。但是,像您一样,在某种程度上仍不能完全避免依赖订购(而且我认为无法避免这种依赖性)。如果不保持替换顺序,最终可能会用“ buzzfizz”而不是必需的“ fizzbuzz”替换15的倍数。 。一种是维护映射对象的有序列表,并针对每个输入依次运行所有对象。如果有两个或多个触发,则将结果连接在一起,并由客户端代码适当地排序。

或者,您可以在第一个“触发”的位置停止(即第一个为此,“条件”返回true)。这就要求客户端代码包含一个“ FizzBu​​zzer”,它将“激发”为15的倍数。我不想在循环中使用丑陋的条件代码来决定是否已经发生转换,或者使用默认转换(即仅打印数字),而是希望将默认转换添加到列表的末尾的变换:

class DefaultMap : implements Substitute {
    Boolean condition(int) { return true; }
    String transform(int val) { return String.valueOf(val); }
};

// ... in main:
Mapper map;

map.add_sub(new FizzBuzzer);
map.add_sub(new Fizzer);
map.add_sub(new Buzzer);
map.add_sub(new DefaultMap);

for (int i=0; i<100; i++)
    System.out.println(map.execute(i));


就代码本身而言,我编写它的方式相当冗长。尽管我不太了解Java 8来实际编写代码来做到这一点,但我相当有信心将它允许将替代写成lambda表达式,这样可以在不损失一般性的情况下大大减少冗长程度(并且鉴于我的无知Java 8,如果它也不允许其他改进的话,我会感到惊讶。

评论


\ $ \ begingroup \ $
我喜欢这个,实际上我正在尝试创建整个解决方案。我一定会制作一个小的库类来容纳不同的选项,然后我将使用它来创建后续步骤!
\ $ \ endgroup \ $
–skiwi
2014年7月12日在12:36

\ $ \ begingroup \ $
如果将类转换为谦虚方法会更好... java 8,您知道。少企业y。
\ $ \ endgroup \ $
–迈克尔·戴尔德(Michael Deardeuff)
14年7月13日在5:03

\ $ \ begingroup \ $
我认为Java没有foreach。
\ $ \ endgroup \ $
–西蒙·匡(Simon Kuang)
14年7月23日19:00

\ $ \ begingroup \ $
@SimonKuang:“请注意,这并非旨在用作可编译Java,而只是类似于Java的伪代码”。为了避免引入类似于C ++的新关键字,Java只是扩展了for语法以提供类似foreach的语义。
\ $ \ endgroup \ $
–杰里·科芬(Jerry Coffin)
2014年7月24日14:37

\ $ \ begingroup \ $
OMG ..为什么将简单的事情变得如此复杂?
\ $ \ endgroup \ $
– Bandu
16年4月13日在14:28

#3 楼

对于像这样的简单用例,StringBuilder太过分了。请改用简单的串联:

String result = "";
if (value % 3 == 0) {
    result = "Fizz";
}
if (value % 5 == 0) {
    result += "Buzz";
}


这里的toDefault周围的括号是不必要的:仅带有max参数的便捷生成器,默认情况下使用min=1

return (toDefault) ? String.valueOf(value) : stringBuilder.toString();


如何添加一些单元测试:

static Stream<String> fizzBuzz(final int max) {
    return fizzBuzz(1, max);
}


#4 楼

我将引用此答案,因为您在解释需求时犯了同样的错误。虽然,我不确定有多少名面试官会有所不同。您应该准备好解释为什么在已实现的解决方案上使用value % 3 == 0 && value % 5 ==0还是对15的值进行硬编码更好。 100.但是对于三倍的倍数,请打印“ Fizz”(而不是数字),对于五倍的倍数,请打印“ Buzz”。对于三到五的倍数的数字,打印“ FizzBu​​zz”

但是您已经实现了这一点:

编写一个程序,打印从1到100的整数。三个打印数字“ Fizz”(而不是数字)的倍数,以及五个打印文本“ Buzz”的倍数。对于三个和五个的倍数的数字,请同时打印两个的串联。

为什么重要?如果您将问题视为客户提供的业务逻辑,则大约为部署解决方案5秒后,客户会回来:“啊,是的,我忘了,如果它可以被3和5整除,则必须打印FixBugz,因为我们无法更改的一些旧版应用程序中有错字他们的解析代码。”现在,不只是将array(3 =>​​'Fizz',5 =>'Buzz',15 =>'FizzBu​​zz')更改为array(3 =>​​'Fizz',5 =>'Buzz',15 =>'FixBugz ')您必须更改一大堆实现代码和单元测试。

我对Java不太满意,但是我喜欢此答案的实现,因此我将做一点改进对它。 FizzBu​​zzify应该采用一些参数,而不是对所有这些幻数进行硬编码。我将在另一个FizzBu​​zz问题的答案中对此进行详细说明。
private static String fizzBuzzify(final int value, final int fizzDivisor, final int buzzDivisor) {
  if (value % fizzDivisor == 0) {
    return (value % buzzDivisor == 0) ? "FizzBuzz" : "Fizz";  
  }
  return (value % buzzDivisor == 0) ? "Buzz" : Integer.toString(value);
}

我建议将fizzDivisorbuzzDivisor设置为具有默认值的可选参数。我只是不知道在Java中是如何做到的。

评论


\ $ \ begingroup \ $
客户同样有可能告诉您Buzz应该更改为Bugz,并期望FizzBu​​zz的打印内容会相应更改。您想得太多了。
\ $ \ endgroup \ $
– RemcoGerlich
2014年7月12日12:50

\ $ \ begingroup \ $
我想每个人都认为FizzBu​​zz @RemcoGerlich。那就是为什么我说能够解释您的思考过程很重要;不管是什么
\ $ \ endgroup \ $
–RubberDuck
2014年7月12日13:11



#5 楼

除了StringBuilder,我同意您的解决方案。

我写了一个完全不同的解决方案,我认为它更“实用”。它可能不是最有效的解决方案,但是此代码可用于不同的问题。

/**
 * @return a function that simply returns its input value, except that it 
 *         returns {@code overWriteValue} each time the function as been
 *         called a multiple of {@code periodicity} times.
 */
public static <T> Function<T, T> overWriter(int periodicity, T overWriteValue) {
    AtomicInteger counter = new AtomicInteger(0);
    return value -> (counter.getAndIncrement() % periodicity) == 0  
                        ? overWriteValue 
                        : value;
}

public static void main(String[] args) {
    Stream<String> ints = IntStream.range(0, 100).mapToObj(Integer::toString);
    Function<String, String> fizzBuzzOverWriter = 
            overWriter(3, "Fizz")
            .andThen(overWriter(5, "Buzz")
            .andThen(overWriter(15, "FizzBuzz")));
    Stream<String> fizzBuzz = ints.map(fizzBuzzOverwriter);
    fizzBuzz.forEach(System.out::println);
}


请注意,选择覆盖值时我根本不使用整数值。您必须确保Stream的起始值和初始计数器值一致。 (我也可以将初始计数器值作为参数添加到overWrite()。)

我也可以将overWrite函数设置为:Integer-> String。但是,如果有的话,那么代码将很难重用。同样,组成三个不同的重叠条件也不会那么简单。

评论


\ $ \ begingroup \ $
至少对我来说,这看起来有点聪明,而且太容易使用,看起来像它们应该工作,但实际上会完全失败。尽管如此,这还是一个有趣的解决方案。
\ $ \ endgroup \ $
–杰里·科芬(Jerry Coffin)
2014年7月12日在16:38

\ $ \ begingroup \ $
@Jerry Coffin您能解释一下吗?我已经习惯了函数式编程(Scala),所以这对我来说根本不是“聪明”的。那“看起来他们应该工作”呢?这段代码立刻给了我正确的答案。我不必进行任何调试。
\ $ \ endgroup \ $
–toto2
2014年7月12日在16:44



\ $ \ begingroup \ $
@Jerry Coffin顺便说一句,我的代码与您的代码几乎相同,但是由于我使用的是Java 8,所以更简短。函数是您的替代品,函数组成(andThen)几乎等同于您的Mapper。
\ $ \ endgroup \ $
–toto2
2014年7月12日在16:47



\ $ \ begingroup \ $
我说的是OverWriter使用它自己的内部计数器这一事实,因此,如果(例如)您尝试从另一个数字(例如25)而不是1开始获得一个序列,则输出将完全错误。
\ $ \ endgroup \ $
–杰里·科芬(Jerry Coffin)
2014年7月12日17:08



\ $ \ begingroup \ $
是的,这里有四个独立递增的计数器是一种代码味道。
\ $ \ endgroup \ $
– Ben Voigt
14年7月13日在16:35

#6 楼

我个人认为最好在第一个if中用第二个术语测试可除性(并且您不禁止三进制),因此,

private static String fizzBuzzify(final int value) {
  if (value % 3 == 0) {
    return (value % 5 == 0) ? "FizzBuzz" : "Fizz";  
  }
  return (value % 5 == 0) ? "Buzz" : Integer.toString(value);
}


这种方式没有该方法中的额外临时工。

#7 楼

我将使用enum来定义替换,例如:

private static enum Transformer implements IntFunction<Optional<String>> {
    FIZZ {
        @Override
        public Optional<String> apply(int value) {
            return value % 3 == 0 ? Optional.of("Fizz") : Optional.empty();
        }
    },
    BUZZ {
        @Override
        public Optional<String> apply(int value) {
            return value % 5 == 0 ? Optional.of("Buzz") : Optional.empty();
        }
    };
}


所以现在您可以轻松地向列表中添加新的“转换”。

为了将int转换为Scala中正确的String,我们将简单地使用flatMap,因为Option也是一个集合。 Java没有这种运气。我想出了这个东西,这有点丑陋,但我认为它很清楚:然后,我使用transformer方法将所有当前实例附加到apply。如果`StringBuilder为空,也只需附加当前整数。

另一种方法(不确定它是否更好)是:

private static String transform(final int i) {
    final StringBuilder sb = Stream.of(Transformer.values()).
            map(t -> t.apply(i)).
            collect(StringBuilder::new, (builder, v) -> v.ifPresent(builder::append), StringBuilder::append);
    if (sb.length() == 0) {
        sb.append(i);
    }
    return sb.toString();
}


这里我们执行Stream<Optional<String>>得到collect,然后将StringBuildermap输出到Stream<Optional<String>>。然后,我们使用(通常被忽略)filter absent首先生成结果map,然后如果连接的Stream<String>为空,则返回collectingAndThen

现在的主要工作变得很简单:

private static String transform(final int i) {
    return Stream.of(Transformer.values()).
            map(t -> t.apply(i)).
            filter(Optional::isPresent).
            map(Optional::get).
            collect(collectingAndThen(joining(), s -> s.isEmpty() ? Integer.toString(i) : s));
}


#8 楼

它说“打印”,所以我将使用sysout。两种解决方案都是恕我直言,而且也相对简短且易于阅读。可能不正确,但我认为这会更好。 br />
FizzBuzzConsumer采用int值并确定要打印的内容。不管int-Value是否为3和5的倍数,因为该条件可以分别由每个条件满足。

整数,每个整数都被输入到FizzBuzzConsumer中。 />
class FizzBuzzConsumer implements IntConsumer {

    @Override
    public void accept(int value) {
        if (value % 3 == 0) {
            System.out.print("Fuzz");
        }
        if (value % 5 == 0) {
            System.out.print("Buzz");
        }
        if (value % 3 != 0 && value % 5 != 0) {
            System.out.print(value);
        }
        System.out.println();
    }
}


再次创建整数流。它看起来(“窥视”)每个对象,以决定是否打印什么,然后换行。它和上面差不多,但是比较冗长。

对我来说似乎都不是很有趣。在这两种情况下,我都认为针对相反条件(peek)重复当前int值的比较将获得最高的可读性。

评论


\ $ \ begingroup \ $
谢谢,我添加了一些有关编写这些确切代码的动机。我想知道对他们来说,什么不是特别好,如果不是很麻烦的话。
\ $ \ endgroup \ $
– HS_Tri
14年7月14日在10:29

\ $ \ begingroup \ $
如果您想对代码进行审查,则应该发表自己的问题。
\ $ \ endgroup \ $
–RubberDuck
14年7月14日在10:35

\ $ \ begingroup \ $
再次感谢,希望我能发表所有意见。我仍然要假设“ FizzBu​​zz”是“ Fizz”和“ Buzz”的串联,但我可能错了!
\ $ \ endgroup \ $
– HS_Tri
14年7月14日在11:01