函数式应用(1)——前奏:聊聊Java8

    肯定有人会问,马上都Java11了,还在聊8,是不是太low了,要知道业内经常流传“线上用的JDK7甚至JDK6,JDK8还没用熟,JDK9都不知道做了啥改进,JDK10也才发布不久,JDK11又要来了”。其实Oracle用了近3年时间才发布了Java8,从7到8,在Java史上也是比较有代表意义的一次改进,其中的很多特性都让java在和其他语言争夺“哪种语言是最好的”问题上增加了说话的资本,下面我们一一道来。

1.Lambda expressions

    首先我们说说Lambda表达式,这里首先提及一个概念叫函数接口,其实就是只包含一个抽象方法的接口,例如我们的Runnable接口,只有run抽象方法。开发者可以用Lambda来传递行为,大家都知道在Java开发中,类和对象是我们首先要考虑的,我们没法直接传递行为,我们只有通过对象包含一个行为进行传递,用lambda就可以让开发人员更专注于业务的开发上,不用每次都写接口和类,而且他还有很多特异功能,例如类型推断、闭包等等,可以让你的代码更优雅,但是Java语言的要求是类才是第一公民,这一点是融入骨子里的,解决不了的,那怎么办?Oracle那帮人很聪明,定义了一些类型来持有lambda表达式,Java里提供了Predicate/Consumer/Function/Supplier/BinaryOperator等类型,他们都在java.util.function下,这些类是不是看起来有些熟悉,在Guava里也是存在的,Google那帮大牛觉得Java的发布太慢了,他们借助匿名类和泛型实现了一套近似lambda的组件,但是使用过的同学肯定深有感触,你会发现用这种方式实现的代码冗长、混乱、可读性差而且非常低效,因为他仅仅是对Java的一层包装而已。
“talk is cheap,show me the code”,让我们来看一个例子:

// 这是提交一个任务到线程池
AsyncTaskManager.submit(new Runnable() {
    @Override
    public void run() {
        checkSuddenInfoWithZip(infoData, param);
    }
});

在这个实现中,infoData和param一定要是声明成final的,不过这种要求也是应该的,对业务逻辑也没啥影响,但是我们要写大量的样板式代码,很繁琐,用lambda写起来是这样的:

AsyncTaskManager.submit(() -> checkSuddenInfoWithZip(infoData, param));

    在这里,你关注的将只是run里面的逻辑,而不用去关心这个对象是什么,lambda里不需要你一定使用final变量,只要变量在事实上是final的就可以使用!

    再举个例子,你有个计算器程序,可以支持加减乘除,你一定会这么做:

public Integer execute(Integer left, Integer right, Operate<Integer, Integer> object) {
	return object.operate(left, right);
}

public static void main(String[] args) {
	// addition
	execute(1,1,new Operate<Integer, Integer>() {
	    @Override
    	    public Integer operate(Integer left, Integer right) {
        	return left + right;
    	    }
	});
	// subtraction
	execute(1,1,new Operate<Integer, Integer>() {
	    @Override
    	    public Integer operate(Integer left, Integer right) {
        	return left - right;
    	    }
	});
}

public interface Operate<T, T> {
	T operate(T left, T right);
}

    为了实现这个功能,你要建一个Operate接口来定义操作行为,然后在执行的地方new个对象出来,这就是前面说的没法传递行为,你得用对象包裹着,换了lambda就这样写了

public static Integer execute(Integer left, Integer right, BinaryOperator<Integer> object) {
    return object.apply(left, right);
}

public static void main(String[] args) {
    execute(1, 1, (left, right) -> left + right);
    execute(1, 1, (left, right) -> left - right);
}

    我们不用定义接口来持有行为,我们的jvm也不用加载Operate接口,这里只有一个操作类型,你可能看上去一样,都要加载一个Class,但是如果还有类似的行为,你还要写更多的接口,加载更多的类,而lambda里只需要加载BinaryOperator这一个类就可以了,后面我们说到Stream再看看lambda的好处。、

    对了,这里还有个非常方便的利器叫方法引用,方法引用其实就是用来简写lambda表达式中的方法,例如我们的Person类有个静态的compare方法

public static int compare(Person a, Person b) {
    return a.age.compareTo(b.age);
}

执行排序的时候可以:

Arrays.sort(personList, Person::compare);

同样,System.out.printlin可以用在stream遍历的时候打印字符串:

list.forEach(System.out::println);

2.类型推断的增强

    泛型可以做到参数化类型,使代码的可扩展性大大增强,但是在Java8之前,方法内套用泛型方法,是无法识别的,你需要显示的赋值给一个具体类型变量,才能推断出来真实类型,而在Java8里不再需要这样做:

public class Value<T> {
    public static <T> T defaultValue() {
        return null;
    }

    public T getOrDefault(T value, T defaultValue) {
        return (value != null) ? value : defaultValue;
    }

    public static void main(String[] args) {
        final Value<String> value = new Value<>();
        value.getOrDefault("22", Value.defaultValue());// jdk7这里会报错,怎么样,java8是不是很智能
    }
}

3.Stream

     终于说到Stream了,该特性的引入可以说是Java史上最厉害的完善,没有之一!她可以让开发者能够快速写出更加有效、简洁和紧凑的代码。

    流的主要思想是将外部迭代改为内部迭代,开发者只需关注迭代的动作而不用关心如何迭代。我们每次迭代Collection时,都要写样板式的for循环,甚至几层嵌套,让代码的可读性差到令人发指,稍有不慎就会出错。

    举个例子来冲击一下你的思维:我们的客户端长连接系统会打包司机心跳数据并通过MQ发送给各业务方,假设现在是100个心跳一个包,业务上现在需要判断,筛选出杭州司机的心跳点,我们会这么做

List<Location> filterResult = new ArrayList<>();
for (Location hb:heartbeats) {
	if (hb.cityId.equals("hangzhou")) {
		filterResult.add(hb);
	}
}

现在需求改了,我们还要加限制,是要杭州男司机的心跳:

List<Location> filterResult = new ArrayList<>();
for (Location hb:heartbeats) {
	if (hb.cityId.equals("hangzhou") && hb.sex.equals("male")) {
		filterResult.add(hb);
	}
}

    突然,tcp说我们现在逻辑变了,每个包里有1000个心跳点了,此时,你怎么办?还这样顺序处理1000个么?假设tcp包里有10000了呢?将这份代码改成并发处理,相信很多人都能做到,但是很麻烦。

现在看看Stream会怎么做:

heartbeats.stream().filter(hb -> hb.cityId.equals("haengzhou").collect(Collectors.toList());修改后增加属性判断后:
heartbeats.stream().filter(hb -> hb.cityId.equals("hangzhou") && hb.sex.equals("male")).collect(Collectors.toList());修改并发后:
heartbeats.parallelStream().filter(hb -> hb.cityId.equals("hangzhou") || hb.sex.equals("male")).collect(Collectors.toList());

    代码看起来是不是简洁很多?一眼看去就知道是过滤(filter)出来杭州的男性司机!有没有感受到lambda带来的好处?把迭代的过程交给jvm吧,你要做的只是选择迭代规则。

    假设我们又来新业务了,需要收集心跳里的各城市心跳数量统计,形如list->map<hangzhou,100>这样的结构,老的方式应该要

Map<City, Integer> count = new Hashmap<>();
for (Location hb:heartbeats) {
	Integer cityCount = count.get(hb.cityId);
	if (cityCount == null) {
		count.put(hb.cityId, 1);
	} else {
		count.put(hb.cityId, cityCount+1);
	}
}

    这里穿插一个新功能,Java8里的Hashmap增加了例如getOrDefault/putIfAbsent/computeIfAbsent/computeIfPresent等方法来简化我们的代码,上面的get就可以用getOrDefault(hb.cityId, 0)来替代。

而lambda里就没这么麻烦了:

Map<City, Integer> count = heartbeats.stream().collect(Collectors.groupingBy(hb -> hb.getCityId(), Collectors.counting()));

    有没有觉得很棒,能用一行代码解决的事,绝对不会用两行!我们从循环里逃出来了,只需要关注循环内的逻辑,关注怎么处理我的每个对象就可以了。

Filter、Transfer、Convert是函数式编程里比较重要的三个利器!映射到Java8里就是filter、map、collect。

    Stream里有个非常重要的概念——惰性求值,这个概念就类似于Builder模式,只有到最后一步遇到调用build方法时,才会去组装对象,Stream里也一样,只要是返回stream对象的方法,都是惰性的,就不会真正执行运算,直到遇到返回非stream对象的方法,这种方法有自己的名字,有的地方叫“及早求值”,有的叫“严格求值”,都是一个意思,这样做的好处是jvm会给你优化执行过程,不会做多余的遍历运算,例如以下两个语句的执行过程是一样的:

xx.stream().filter(hb -> hb.cityId.equals("hangzhou") && hb.sex.equals("male")).collect(Collectors.toList());
xx.stream().filter(hb -> hb.cityId.equals("hangzhou")).filter(hb -> hb.sex.equals("male")).collect(Collectors.toList());

    Stream提供了filter、sort、map、match、count、reduce等等很多功能,可以满足大家日常所有的开发需要,用stream的好处显而易见,所以不要再自己写循环了,用优雅、可读性高的Stream+lambda来替代吧!
    刚刚一直在说list接口的stream方法,按照惯例,接口里增加了方法,实现类里应该都要增加实现的,如果我们在业务里实现了我们自己的一种AbcdList,如果直接升级Java8,岂不是编译就报错了?Oracle那帮大牛怎么解决你自己实现的List能兼容新加的stream方法呢?Java从一开始就声称要“永远向下兼容”,就是这句话,把自己带坑里了,哈哈!开玩笑的。。。回归正题,怎么解决呢?他们在接口里增加了一个特性-默认方法
 

4.默认方法

    默认方法可以做到在不破坏二进制兼容性的前提下,给接口增加方法,就是不强制那些实现了该接口的类也同时实现这个新加的方法。

@FunctionalInterface
public interface FunctionalDefaultMethods { 
    void method();

    default void defaultMethod() {            
    	System.out.println("this is default method!");
    }        
}

 

    这个特性在多态里用起来非常爽,当需要增加某个特性的时候,再也不用给每个实现类增加样板式的方法了,特别是例如命令模式时,增加了一个命令,你要给每个实现类增加抛NotSupport异常的样板代码,现在你只要在接口里做默认实现抛NotSupport异常就可以了,所有的实现类将自动继承这种实现!现在脑海中是不是回想起当初一个类一个类的加NotSupport的痛苦日子了。

    接口的默认方法还有什么好处呢?它间接的解决了多重继承问题,但是Java8里的这种多重继承更倾向于解决的是代码块的继承,而不是对象状态的继承,这一点也完美避开了多重继承导致的二义性等一系列诟病。
 

5.Optional

    Optional主要是用来替换null值的,使用他的目的主要是提示开发者使用之前要检查是否为空,避免NPE的发生!其实这个概念早在之前就出现在了Guava里,Google那帮大牛又一次教会我们:觉得不爽,咱们就自己干!

    其实现在应用更多的应该还是Guava的Optional,特别是在对外api接口上,因为你没法要求你的调用方升级到1.8,但是你可以让他们去依赖Guava,当然,系统内部随便你用哪个,开心就好!

6.结束

    Java8的主要功能就先介绍到这里,其他的一些改进,例如Hashmap和ConcurrentHashmap的改进、新引入的Date-time api、注解的扩展、Nashorn JavaScript引擎等特性也很不错,不过有的应用的场景比较特殊,就不一一介绍了。

    如果深究Java8算不算函数式的,众说纷纭,纠结来纠结去也没啥意义,我个人觉得:如果我可以用函数式的思维来做逻辑实现,而且用起来爽、结构清晰,这就足够了,管他是什么语言,下一篇我们就一起探讨一下函数式编程,以及如何改变我们的编程思维,从命令式到函数式的蜕变!

    Java8跑起来吧,你的升级总不能比Java版本的升级节奏还慢吧~~~

这里有IT新同学交流群:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值