函数编程实践记(3)——完美数

“舅,上次的单词统计不过瘾啊” YY 说到
“要是觉得单词统计不过瘾的话,那就再看一个例子, 来,来,来,喝杯酸奶提提神!! ” 顺手把刚买的菊乐丢给了YY

题目

完美数分类
古希腊数学家Nicomachus发明了一种自然数的分类方法, 任意一个自然数都唯一地被归类为过剩数(abundant)、完美数(perfect)或不足数(deficient)。一个完美数的真约数(即除了自身以外的所有正约数)之和, 恰好等于它本身。例如 6 是完美数, 因为他的约数是 1, 2, 3, 而6 = 1 + 2 + 3; 28也是完美数,因为 28 = 1 + 2 + 4 + 7 + 14。
所以 可以得到自然数分类规则

完美数 真约数之和 = 数本身
过剩数 真约数之和 > 数本身
不足数 真约数之和 < 数本身
问,给定的一个数判断是否是完美数
进一步, 求出 1- 100之内所有的完美数

当YY看到题目时候,嘴角微翘,说: ”切, 小儿科“。 此时我的表情是这样。我的表情

【木丁糖 http://blog.csdn.net/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。

第一个Java版本

YY, 你就说说,你的思路吧, 我问到。
” 很简单啊, 按照题意来啊。
1. 找出真约数,并存储
2. 对所有真约数求和
3. 判断真约数之和与本尊是否相等, 返回 true , false

不得不说YY的脑子没有被王者农药吃掉, 思路清晰!

private static boolean isPerfectNumber( int targetNumber) {

        //找出所有的真约数 存ArrayList
        List<Integer> aliquotList = new ArrayList<>();
        aliquotList.add(1); //1肯定是,直接add到list

        for(int i = 2; i < targetNumber; i++) {//从2开始,没毛病
            if(targetNumber % i == 0) {
                aliquotList.add(i);
            }
        }//for循环搞定所有真约数

        int aliquotSum = 0;

        for(int i = 0; i < aliquotList.size(); i++) { //循环真约数list,求和
            aliquotSum += aliquotList.get(i);
            //out.print(aliquotList.get(i) + " ");
        }

        if(aliquotSum == targetNumber) {//判断求和是否跟本尊相等
            return true;
        }

        return false;

    }

判断100是不是完美数,如下

public static void main(String ... args) {
      out.println(isPerfectNumber(100));

    }

看到情况,立即追问 ”YY, 你这是小学生水平啊,是不是打游戏被小学生坑惨了,已经同化了啊?“
”哪有,哪里不对吗?“ YY问到
”这个好歹是需要给别人使用的撒, 是不是要包装包装哦“,我鄙夷的说到。
”哦,懂起了,那就面向对象编个程“ ,YY 说罢,就撸起了代码。

【木丁糖 http://blog.csdn.net/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。

第二个Java版本——面向对象

public class ImpNumberClassifierSimple {

    private int mNumber;

    private Map<Integer, Integer> mCache;

    public ImpNumberClassifierSimple() {}

    public ImpNumberClassifierSimple(int targetNumber) {
        mNumber = targetNumber;
        mCache = new HashMap<>();
    }

    public boolean isFactor(int potential) {
        return mNumber % potential == 0;
    }

    public Set<Integer> getFactors() {
        Set<Integer> factors = new HashSet<>();

        factors.add(1);
        factors.add(mNumber);

        for (int i = 2 ; i < mNumber; i++) {
            if (isFactor(i))
                factors.add(i);
        }

        return factors;
    }

    public int aliquotSum() {
        if (mCache.get(mNumber) == null) {
            int sum = 0;

            for (int factor : getFactors()) {
                sum += factor;
            }
            mCache.put(mNumber, sum - mNumber);
        }

        return mCache.get(mNumber);
    }

    public boolean isPerfect() {
        //out.println("aliquotSum: " + aliquotSum());
        return aliquotSum() == mNumber;
    }

    public boolean isAbundant() {
        return aliquotSum() > mNumber;
    }

    public boolean isDeficient() {
        return aliquotSum() < mNumber;
    }

}

测试代码:

//version 2 OOP implements
        for (int index = 1; index <= 100; index++) {
            if(new ImpNumberClassifierSimple(index).isPerfect()) {
                out.println(index);
            }
        }

看完代码,我心里说到,”可以啊,这小子,想的东西比较多啊,还有缓存? 得好好问下“。
”YY, 这次整这么多,说说的小九九吧“
”舅,你不是说,要面向对象嘛,还得给别人好用,所以 就写了一个类, 而且用到了缓存,后续有重复的数字出现,就不用再去执行一次,提高效率。“ 说完, YY得意的笑笑。
”1. Map<Integer, Integer> mCache 用于存储数字 和 他对应的真约数之和, 比如《6,1+2+3》 《4, 1+2》。。。
2. isFactor(int potential)方法是用于测试 potential 是不是 潜在的因数
3. getFactors 是获得所有的因数,包括数值本身。
4. aliquotSum 得到真因数之和,注意其中的 mCache.put(mNumber, sum - mNumber); 因为之前加了数字本尊,所以需要减去

“YY, 怎么想到把代码写的这么松散, 分门别类?” 我问到
”有利于单元测试啊, 是不是很屌?“ YY回复到
”我,呸。。。“

”YY, 你不觉得,每次判断需要new 这么多的类,new ImpNumberClassifierSimple(index) 很费内存的啊, 还有 整体实现很好,但是不足的是,存在了中间变量“
”啥? 这是基本的OOP写法啊,里面有成员变量,局部变量啊“ YY 不解的问
”是,你说的没错, 但是我是教你 函数式啊,如果是OOP,我看你已经很ok了“ 我不屑的说
”那该怎么处理哦? 总不能,不要成员变量吧?“ YY嘟囔道
”额,可以最小化的使用,成员变量是整个类对象共享的,这次的小目标是 最小化共享状态“
”那,怎么做?“ YY 懵比了
”去掉成员变量 mNumber 那就改用传参数的形式呗“ 我提醒道
”OK, 我试试“ YY,风一样的跑到电脑跟前,此时,只听到键盘 噼里啪啦的声响
不一会, YY 完成了 他的第三版java程序

【木丁糖 http://blog.csdn.net/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。

第三个Java版本——稍微靠近函数式编程

public class ImpNumberClassifier {

    //1. 众多方法都必须加上number参数,因为没有可以存放他的内部状态
    public static boolean isFactor(final int candidate, final int number) {
        return number % candidate == 0;
    }

    //2. 所有方法都带public static修饰,因为他们都是 【纯函数】, 并因此可以在完美分数之外的领域使用
    public static Set<Integer> factors(final int number) {
        Set<Integer> factors = new HashSet<>();

        factors.add(1);
        factors.add(number);

        for (int i = 2 ; i < number; i++) {
            if (isFactor(i, number))
                factors.add(i);
        }

        return factors;
    }

    //3. 注意例子中对参数类型的选取,尽可能宽泛的参数类型可以增加函数重用的机会
    public static int aliquotSum(final Collection<Integer> factors) {
        int sum = 0;
        int targetNumber = Collections.max(factors);

        for (int factor : factors) {
            sum += factor;
        }

        return sum - targetNumber;
    }

    //4. 例子目前在重复某个数值分类操作的时候效率比较低, 因为没有缓存
    public static boolean isPerfect(final int number) {
        //out.println("aliquotSum: " + aliquotSum());
        return aliquotSum(factors(number)) == number;
    }

    public static boolean isAbundant(final int number) {
        return aliquotSum(factors(number)) > number;
    }

    public static boolean isDeficient(final int number) {
        return aliquotSum(factors(number)) < number;
    }

}

测试代码

//version 3 static class implements
        for (int index = 1; index <= 100; index++) {
            if (ImpNumberClassifier.isPerfect(index)) {
                out.println(index);
            }
        }

”哎呦,不错哦,还说明了四个注意点,了然! 只不是,我注意到没有缓存,这是为什么?“ 我疑惑问
”哦,因为这版本不使用共享状态,缓存意味着持续存在的状态“ YY解释道
”没事,先不考虑缓存的问题。 这个算是比较 函数式的写法了, 给你点赞“ 我高兴的说道

YY, 写了这么多,想不想,实践下如何使用Java 8来做这个小题目?“ 我问道
“想啊,就是要跟大神学习函数式”YY 睁着大眼睛说到
“拍马屁是不行的,好学才是王道”我正经回复到,心里美滋滋的

【木丁糖 http://blog.csdn.net/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。

Java8实现

public class ImpNumberClassifierJava8 {

    //求某个数的所有因子, 包括 1 和number本身, 返回的是一个IntStream
    public static IntStream factorsOf(final int number) {

        return IntStream.rangeClosed(1, number)
                .filter(potential -> number % potential == 0); //懵比地方
    }

    //求所有真因数之和,出去number本身
    public static int aliquotSum(int number) {

        return factorsOf(number).sum() - number;
    }

    //判断 是否是完美数
    public static boolean isPerfect(int number) {
        return aliquotSum(number) == number;
    }

    public static boolean isAbundant(int number) {
        return aliquotSum(number) > number;
    }

    public static boolean isDeficient(int number) {
        return aliquotSum(number) < number;
    }

}

测试代码

        //version 4 java8 实现
        IntStream.rangeClosed(1, 100)
                .forEach(number -> {
                    if (ImpNumberClassifierJava8.isPerfect(number)) {
                        out.println(number);
                    }
                });

“舅,懵比了,懵比了?” YY 困惑了
“哪里懵了? 是API不熟悉,还是啥?” 我问到
IntStream.rangeClosed(1, number).filter(potential -> number % potential == 0); 这句,特别是其中的filter内部的写法, 好奇怪” YY 说到

舅Tips
IntStream是BaseStream的一种,Stream是Java 8新引入的流。是对集合操作的更高层次的抽象。
它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。参考: https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/

“YY, 不要纠结IntStream是什么,简单点, 就是在集合基础上进行操作,先稍微认识IntStream中的几个API方法,就行了。
其一, rangeClosed(1, number) 表示说,我要创建一个 1 - number 之间的数列 [1, number] (stream流)
其二, filter,顾名思义,就是过滤,给定的过滤条件是 number % potential == 0, 其中potential无所谓,你写成YY都行” 我说到,看到YY还是很困惑,继续说到:
“这么理解, 我们要求出某个数number,比如100这个数, 能被100整除的所有的数,然后保存到某个stream中返回给外界使用, 你肯要构建 1- 100数列,然后使用判断条件挨个判断,找出符合条件的数字,并保留起来撒。 ”
“恩, 这个思路我知道,就是其中的 filter(potential -> number % potential == 0) 不好理解” YY说道
“哦,这个是Lambda表达式,其实我们可以把他变为我们熟悉的 匿名内部类的方式”我说
“舅,搞快,变一个”YY, 催促的说
“F*ck, 这么麻烦, 好, 变不出来,还以为我是水货啊。走起~”

【木丁糖 http://blog.csdn.net/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。

Lambda表达式前世今生

估计和YY有同样疑问的不在少数,那就来说下, Lambda表达式。它是一种紧凑的、传递行为的方式

第一个Lambda表达式

不论是Android,还是Swing, 都会有这样的用法: 响应用户操作,需要注册一个事件监听器,用户一点击,监听器就会执行一些操作。以Android 中点击某个Button按钮,打印Log为例说明。

button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d("YY", "YY 戳了你一下!");
            }
        });

非常简单的代码,点击button按钮,打印一句话。匿名内部类实现了一个叫onClick的方法, 我们给按钮传递了一个代表某种行为的对象。 对不对?

设计匿名内部类的目的,就是为了方便Java程序员将代码作为数据传递, 不过匿名内部类不够简便。其中重要的是代码是什么? 是 Log.d("YY", "YY 戳了你一下!");, 为了这个,不得不加上 4 行冗繁的样板代码, 而且代码相当难读,因为它 没有清楚地表达程序员的意思。 我们不想传入对象,只想传入行为, 好吗????, 不是吗?

button.setOnClickListener(YY要行为,不要给我对象)

所以,Lambda表达式 是传递行为的,而不是传递对象! 所以,上述button代码可以简洁为:

button.setOnClickListener(view -> Log.d("YY", "YY 戳了你一下!"));

我们传入的是什么? 是一段代码块——一个没有名字的函数,view是参数, -> 将参数和Lambda表达式的主体分开,而主体是用户点击按钮是会运行的一些代码, 至于参数view为什么不需要显示的申明为View,那是因为Java根据上下文自动推断参数类型!!!

【木丁糖 http://blog.csdn.net/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。

stream.filter中的lambda表达式

“YY, 现在对lambda表达式清楚了没?”
“你这么一说,简单的理解为,就是针对匿名内部了的简化方式” YY 回复
“可以,这么理解。 你你说说,filter中为什么会是那样的写法呢?”
“好勒,看好了,舅!”YY 爽快的回答

IntStream filter(IntPredicate predicate) 参数是一个 IntPredicate类型, 本质是一个接口, 并且有四个方法, 我们只要用到 test方法

public interface IntPredicate {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param value the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(int value);

所以,上述的filter可以这么繁琐的写为:

public static IntStream factorsOf(final int number) {
        return IntStream.rangeClosed(1, number)
                .filter(new IntPredicate() {//匿名内部类方式
                    @Override
                    public boolean test(int value) {
                        return number % value == 0;
                    }
                });
    }

“哈哈,YY, 反映很快的嘛,却是如此。 ”

Lambda表面上是说, 替代匿名内部类,但是深层次的是,思想的转变,从以前传递是匿名内部类的对象,转变为传递的是匿名函数。在函数式里,函数也是第一等公民,YY,你想想,以前java里,有提倡传递函数的吗? 基本是数据对象, 对不对?

“对,老舅 ViVO”
“还OPPO 勒? 不要贫嘴,继续。。”
“舅,Java 8 的基本会了,也初步理解了Lambda表达式的由来,那测试代码中的 forEach也是这么来的?”

IntStream.rangeClosed(1, 100)
                .forEach(number -> {
                    if (ImpNumberClassifierJava8.isPerfect(number)) {
                        out.println(number);
                    }
                });

“当然啊,你看forEach参数的本质是什么,然后可以使用匿名内部类的方式繁琐的写,写完IDE会提醒你,可以使用lambda表达式代替, just do it ”我回复到
“好,明白了,那Kotlin 怎么实现呢?”
“哈哈,好学的 YY, 那就看看Kotlin的实现吧”

【木丁糖 http://blog.csdn.net/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。

Kotlin实现

Kotlin 在语言层面就支持了Lambda表达式,而且,写的更优雅,简约!

fun main(args: Array<String>) {  
    //求1-100以内的完美数,并且打印出来
    (1..100).filter(Int::isPerfect).forEach(::println)
}

// Kotlin允许你扩展Int类的方法
fun Int.isPerfect(): Boolean = (1 until this).filter { this % it == 0 }.sum() == this

“我 * , 这么简洁, 还可以扩展方法?” YY 惊呼
“蛋定,蛋定,这个仅仅是Kotlin的一小部分,YY, 你有没有发现比Java 更简约?”我蛋定的回复
“斯国一(日语),
1. 1-100 可以写成(1 .. 100);
2. 1-100, 不包括100,可以写成(1 until 100)
3. filter条件更简洁 Int::isPerfect
4. forEach打印不用 println(number)
, 简直,碉堡啦!!!

“呵呵,Kotlin的特点,岂止这些,需要你自己去解锁啊,YY”
“恩, 哦你抢(日语)” YY 撒娇道
“哦,哦,你妹,我是你舅!!”

【木丁糖 http://blog.csdn.net/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值