Java重新出发--Java学习笔记(十)--函数化编程

根据原作的推荐,这里虽然没有看过这本书,不过贴出来以便能够日后学习的同时也推荐给更多的人

《Java8实战》

这里简单介绍一点Java8这些特性

(1)用行为参数化把代码转递进去

java8中增加了通过API来传递代码的能力,如果你想要写两个只有几行代码不同的方法,那现在只需要把那部分不同的代码作为参数传递进去就可以了。
但这样想来实在也不好理解,还是要通过具体的例子来进行理解。Java8中这样做起来非常的清晰和简介(不仅限于匿名内部类)
我们现在就开始来举这个例子:

比如现在我们接了一个项目是要过滤苹果的相关属性的。目前一期的需求是要过滤出颜色是绿色的苹果,但到了二期又提出了要过滤重量超过150g的苹果。再往后,可能又会提新的需求,既要绿色也要超过150g.看到这里,你可以暂停先不要往下看,思考一下,如果是你,你会如何选择方法来实现呢。
(在学习之前,我的方法是写一个方法,传进苹果对象, 对其进行颜色和大小的判断。但是现在想来这个方法真的非常局限,一个方法用来判断某些属性是不是符合一个值也许可以,但是如果要对很多个属性做判断,或者判断多种值是不是就要写n个方法了?我一直以来的这种思维确实真的非常不适合扩展代码)

理想状态下,你应该把你的工作量降到最小,此外这种功能实现起来要很简单,而且易于长期维护。
下面我们将完成一个思考的过程,从笨拙中体会思考带来的意义,也体会新特性的好处。

第一次尝试:

    //筛选绿苹果
    public static List<Apple> filterGreen(List<Apple> inventory){
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory){
            if("green".equals(apple.getColor())){
                result.add(apple);
            }
        }
        return result;
    }

这样的代码很好理解,就是过滤绿苹果呗,但是客户现在要求要筛选红苹果了怎么办?简单的方法首先想到就是把函数名和if判断的条件匹配到红色上就好了,然而如过颜色又加了呢?一个良好的原则是在编写类似代码之后,尝试将其抽象化。

第二次尝试:把颜色当成参数
给方法增加参数,将属性灵活化,这样就可以匹配不同的颜色啦。

//根据颜色参数来适应变化
    public static List<Apple> filterAppleByColor(List<Apple> inventory,String color){
        List<Apple> result = new ArrayList<>();
        for(Apple apple:inventory){
            if(apple.getColor().equals(color)){
                result.add(apple);
            }
        }//end for
        return result;
        
    }

现在如果要过滤不同颜色的苹果,只要像下面这样调用就可以了:

List<Apple> greenApples = filterApplesByColor(inventory,"green");
List<Apple> redApples = filterApplesByColor(inventory,"red");

这样想着好像也没有什么难嘛,这个时候问题又来了,客户说我除了颜色之外我还想同时过滤掉轻的苹果,这个时候,你会觉得,那再加进来一个参数好啦,于是代码又变成了这样:

    //根据多种多样的颜色和多种多样的重量来选择
    public static List<Apple> filterApplesByColor(List<Apple> inventory,int weight){
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory){
            if(apple.getWeight() > weight){
                result.add(apple);
            }
        }  // end for
        return result;
    }

这个方法好像看起来好了很多,但是我们注意到,每次调用这个方法的时候,我们都把库存遍历了一遍,再对每个苹果应用筛选条件。这有点儿令人失望了,因为它违反了DRY工程原则(DON'T Repeat Yourself)你可以将颜色和重量结合为一个方法,成为filter,不过就算是这样,你还是需要一种方式来区分某次调用是要筛选哪个属性,你可以加上一个标志位来区分对颜色或重量的查询。先在这里奉劝不要这么做,后面会展开说为什么。
先写一个将所有属性结合起来的笨拙方法尝试如下:

//利用笨拙的方法结合属性尝试
    public static List<Apple> filterApplesFool(List<Apple> inventory,String color,int weight, boolean flag){
        List<Apple> result = new ArrayList<>();
        for (Apple apple: inventory){
            if((flag && apple.getColor().equals(color)) ||
                    (!flag && apple.getWeight() > weight)){
                    result.add(apple);
            }
        }
        return result;      
    }

你可以这么使用,但是真的很没水准。

List<Apple> greenApples = filterApples(inventory, "green", 0, ture);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);

这个我们回过头来看,真的是很糟糕的一段代码,true和false只看代码你能看出来指代了什么意思嘛?这里可能就需要你手动加上很多注释了,但其实这真的是没有必要的注释。此外更关键的一点是如果甲方又提出了新的属性怎么办呢?形状,产地,尺寸,难道要一个一个加进参数中么?那么在你修改的时候,就可能要给很多代码动刀了。真的非常不利于维护。你就会有非常多不同组合的过滤方法或者一个超级冗杂的大方法。到目前为止,你已经给filterApples方法加上了值(如String、Integer或boolean)的参数。这对于某些确定性问题可能还不错,但如今这种情况下,你需要一种更好的方式,来把苹果的选择标准告诉你的filterApples方法。

这里行为参数化就闪亮登场了。让我们后退一步来看更高层次的抽象。一种可能的方案是对你的选择标准进行建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值,我们把它称为谓词(即一个返回boolean值的函数)

第四次尝试:根据抽象条件筛选
让我们先来定义一个接口对选择标准建模:

public interface ApplePredicate{
    boolean test(Apple apple);
}

(或者做一个模糊搜索的功能)

    public interface SearchPredicate{
        List test(String name);
    }

现在你可以使用ApplePredicate的多个实现代表不同的选择标准了。

//仅仅选出绿苹果
public class AppleColorPredicate implements ApplePredicate{
    @Override
    public boolean test(Apple apple) {
        // TODO Auto-generated method stub
        return "green".equals(apple.getColor());
    }
}
//仅仅选出重的苹果
public class AppleHeavyWeightPredicate implements ApplePredicate{

    @Override
    public boolean test(Apple apple) {
        // TODO Auto-generated method stub
        return apple.getWeight()>150;
    }
}

你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。
在这里算法簇就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicateAppleGreenColorPredicate。但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,完成不同的行为。

利用ApplePredicate改过之后,filter方法看起来就是这样的:

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p){
    List<Apple> result = new ArrayList<>();
    for(Apple apple:inventory){
        if(p.test(apple)){
            result.add(apple);
        }  // end if
    }    // end for
    return result;
}

这里值得暂停下来小小地庆祝一下。这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicacte对象就可以了,你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了。

这时我们已经做了一件较为灵活的事情了,filterApples方法的行为取决于你通过ApplePredicate对象传递的代码,换句话说,你已经把filterApples方法的行为参数化了!

第五次尝试:使用匿名内部类
上面的方式已经很不错了,还有什么问题呢?
我们都知道,人们不愿意用那些很麻烦的功能或者概念,目前,当把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会用到一次的ApplePredicate对象。下面这段程序总结了你目前看到的一切,这真的很罗嗦并且浪费时间。

public static void main(String...args) {
        // TODO Auto-generated method stub
        List<Apple> inventory = Arrays.asList(new Apple("green",80),
                                              new Apple("green",155),
                                              new Apple("red",120));
        List<Apple> heavyApple = 
                filterApples(inventory,new AppleHeavyWeightPredicate());
        List<Apple> greenApple = 
                filterApples(inventory,new AppleColorPredicate());
}

费这么大劲儿,真的没什么必要。能不能做得更好呢?Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类,它可以帮助你进一步改善代码,让它变得更简洁:

List<Apple> redApples = filterApples(inventory, new Applepredicate(){
    public boolean test(Apple apple){
        return "red".equals(apple.getColor());
    }
});

GUI应用程序中经常使用匿名类来创建事件处理器对象(下面的例子使用的是JavaFX API,一种现代的Java UI平台):

button.setOnAction(new EventHandler<ActionEvent>(){
    public void handle(ActionEvent event){
        System.out.println("Woooo a click!");
    }
});

但是匿名类仍然不够好。

第一,它往往很笨重,因为它占用了很多空间,有很多模板代码。
第二,很多程序员觉得它用起来很让人费解
这里插一条经点谜题:

public class Puzzle {
    public final int value = 4;
    public void doIt(){
        int value = 6;
        Runnable r = new Runnable(){
            public final int value = 5;
            public void run(){
                int value = 10;
                System.out.println(this.value);
            }
        };
        r.run();
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Muzzle m = new Muzzle();
        m.doIt();//输出什么?
    }
}

答案是5,因为this指的是包含它的Runnable,而不是外面的类Puzzle。

整体来说,啰嗦就不好。它让人不愿意使用语言的某种功能,因为编写和维护啰嗦的代码需要很长时间,而且代码也不易读。好的代码应该一目了然。即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍然不能让人满意。在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate中的test方法或者是EventHandler中的handler方法)。在理想的情况下,我们想鼓励程序员使用行为参数化模式,因为正如你在前面看到的,它让代码更能适应需求的变化,但也同样的,啰嗦不可避免。这也正是Java 8的语言设计者引入Lambda表达式的原因——他让传递代码的方式变得更加简洁、干净。

以上的例子如果拿lambda表达式实现会非常的好看

List<Apple> result = 
    filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

但是基于我们还是单独开一章学习一下lambda表达式的前提,我们这里就先把这个问题收尾,
相信在下一章学习完lambda表达式后,这种优化对你来说根本不是问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270