jdk都更新到21了,java8的函数式编程到底理解没
解耦
首先讲一个编程的原则,解耦。这个概念都说烂了,但是具体怎么做呢。
解耦其实就是解除依赖。
想想十几年前百花齐放的手机充电线,到现在的Type-C,对消费者的好处就是不用带一大堆不同的充电线了,对企业的好处就是不用为生产特定的接口改生产线。
说回到代码上,要怎么解除依赖,解除什么依赖。
简化代码,就是把逻辑和控制分开
先说总结,函数式解决对于状态的依赖,泛型,解决对于类型的依赖。都是对于控制的操作
逻辑,就是指业务逻辑。与语言无关
下面举个例子说明什么是函数式编程,他是如何解除对状态的依赖(说明函数式编程的优势)
函数式编程
它的理念就来自于数学中的代数。
大家耳熟能详的斐波那契数列的函数式表示如下
f(x)=f(x-1)+f(x-2)
对于函数式编程来说,它只关心定义输入数据和输出数据相关的关系,对应数学自变量和应变量。
有状态和无状态
累加
// 非函数式,有状态
int cnt =0;
public void increment(){
cnt++;
}
纯函数累加
如果写成纯函数,应该是下面这个样子。
// 函数式,无状态
public int increment(int cnt){
return cnt+1;
}
这个是你传给我什么,我就返回这个值的 +1 值,你会发现,代码随便拷,而且与线程无关,代码在并行时候不用锁,因为是复制了原有的数据,并返回了新的数据。
字符串处理
再给一个稍微复杂一点的例子
找出偶数;乘以 3;转成字符串返回。
版本1 非函数式
平铺直叙的代码
import java.util.ArrayList;
import java.util.List;
public class Example {
// 定义一个函数 process,处理偶数并返回字符串
public static String process(int num) {
// 过滤掉非偶数
if (num % 2 != 0) {
return null;
}
num = num * 3;
String result = String.format("The Number: %d", num);
return result;
}
// 测试函数
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 处理列表中的元素
List<String> results = new ArrayList<>();
for (int num : nums) {
String result = process(num);
if (result != null) {
results.add(result);
}
}
// 输出结果
for (String result : results) {
System.out.println(result);
}
}
}
我需要手动维护nums的数据,代码阅读上如果没有注释,你也会比较晕。
版本2:函数式代码
public class Example {
// 定义一些函数,用于过滤和映射元素
public static Predicate<Integer> even_filter = num -> num % 2 == 0;
public static Function<Integer, Integer> multiply_by_three = num -> num * 3;
public static Function<Integer, String> convert_to_string = num -> String.format("The Number: %d", num);
// 测试函数
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 过滤元素,映射元素,输出结果
nums.stream()
.filter(even_filter)
.map(multiply_by_three)
.map(convert_to_string)
.forEach(System.out::println);
}
}
看着是不是有点眼熟,那么这段代码还可以简化
版本3 最简化版函数式代码
import java.util.Arrays;
import java.util.List;
public class Example {
// 测试函数
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 过滤元素,映射元素,输出结果
nums.stream()
.filter(num -> num % 2 == 0)
.map(num -> num * 3)
.map(num -> String.format("The Number: %d", num))
.forEach(System.out::println);
}
}
突然间的释怀,好家伙这不就是lambda表达式吗,函数式接口跑哪去了,点进去.map接口看源码
/**
* Returns a stream consisting of the results of applying the given
* function to the elements of this stream.
*
* <p>This is an <a href="package-summary.html#StreamOps">intermediate
* operation</a>.
*
* @param <R> The element type of the new stream
* @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
* <a href="package-summary.html#Statelessness">stateless</a>
* function to apply to each element
* @return the new stream
*/
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
入参其实就是版本2写的函数式接口
减少代码函数只是运用函数式接口附带的,它的主要优势是
特征一、stateless: 函数不维护任何状态。函数式编程的核心精神是 stateless,比如上面filter 操作只考虑当前元素的状态,即判断当前元素是否为偶数,不需要考虑其他元素或外部状态的影响,简而言之就是它不能存在状态,打个比方,你给我数据我处理完扔出来。里面的数据是不变的。
特征二、immutable: 输入数据是不能动的,动了输入数据就有危险,所以要返回新的数据集。
这两个特征带来的效果就是并行无风险。
在累加的例子中,如果有外部变量也操作了cnt的值,程序固定输入就可能得到不同输出,是有风险的。
函数方法的用法
apply()
apply() 方法是 Java 8 中 Function 接口中的一个方法,它接受一个参数,然后将这个参数应用到函数中,返回一个结果。
public double doubleNumber(int a) {
return a * 2.0;
}
public void fun2() {
// 将 doubleNumber 方法作为参数传递给 Function 接口的实现
Function<Integer, Double> doubleFunc = this::doubleNumber;
// Function<Integer, Double> doubleFunc = a -> a * 2.0; 等价
// 应用函数,得到结果 20
System.out.println(doubleFunc.apply(10));
}
andThen()
下面是一个更复杂的例子,用于解释 apply()和 andThen()方法的用法:
复制代码
public void fun() {
// 定义一个 Function 对象,用于将字符串转换为整数
Function<String, Integer> strToInt = s -> Integer.parseInt(s);
// 定义一个 Function 对象,用于将整数加倍
Function<Integer, Integer> doubleInt = i -> i * 2;
// 定义一个 Function 对象,将字符串转换为整数并加倍
Function<String, Integer> strToDoubleInt = strToInt.andThen(doubleInt);
// 将字符串 "10" 应用到 strToDoubleInt 函数中,得到结果 20
Integer result = strToDoubleInt.apply("10");
// 输出结果20
System.out.println(result);
}
andThen() 方法将 strToInt 和 doubleInt 两个函数串联起来得到了一个新的Function。
compose()
它返回一个组合函数,其中参数化函数将首先执行,然后是第一个函数。如果任一函数的计算抛出错误,则会将错误转发给组合函数的调用者。
我理解就是在出入参相同的时候简化了andThen用法
public class Example {
public static void main(String args[]){
Function<Integer, Integer> strToInt = s -> s+1 ;
strToInt = strToInt.compose(i -> i * 2);
System.out.println(strToInt.apply(10));
}
}
出入参不同的示例
import java.util.function.Function;
// Main class
public class Example {
// Main driver method
public static void main(String args[]){
Function<Integer, Double> half = a -> a / 2.0;
Function<String, Integer> strToInt = s -> Integer.parseInt(s);
strToInt = strToInt.compose(i -> i * 2);
System.out.println(half.apply("10"));
}
// 定义一个 Function 对象,用于将字符串转换为整
}7.5
identity()
此方法返回一个函数,该函数返回其唯一的参数。
// Java Program to Illustrate identity() method
// Importing Function interface
import java.util.function.Function;
// Main class
public class Example {
// Main driver method
public static void main(String args[])
{
Function<Integer, Integer> i = Function.identity();
// 输出10
System.out.println(i.apply(10));
}
}
你说,不就原值返回这玩意能有啥用呢
结合示例
把list<Object>转换成HashMap<Object.name,Object>的方法
在工作中是个很常见的操作
public class ListUtils {
public static <T, K, V> Map<K, V> toMap(Collection<T> list, Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends V> valueMapper) {
if (list == null) {
return new HashMap();
} else {
Map<K, V> map = new HashMap();
Iterator var4 = list.iterator();
while(var4.hasNext()) {
T t = (T) var4.next();
if (t != null) {
map.put(keyMapper.apply(t), valueMapper.apply(t));
}
}
return map;
}
}
}
使用
public class Service {
public static void main(String[] args) {
List<DemoDo> list = Stream.of(new DemoDo[]{new DemoDo("zhangsan", "boy"),
new DemoDo("lisi", "dog")}).collect(Collectors.toList());
Map<String, DemoDo> map = tailMnTargetVOMap(list);
// {lisi=DemoDo(name=lisi, desc=dog), zhangsan=DemoDo(name=zhangsan, desc=boy)}
System.out.println(map);
}
public static Map<String, DemoDo> tailMnTargetVOMap(List<DemoDo> list) {
if (null!=list&&!list.isEmpty()) {
return ListUtils.toMap(list, DemoDo::getName, Function.identity());
} else {
return new HashMap<>();
}
}
}
@Data
@AllArgsConstructor
class DemoDo{
private String name;
private String desc;
// 其他属性...
}
泛型的用法
这里在回顾一遍总结
函数式解决对于状态的依赖,泛型,解决对于类型的依赖。
无论哪种程序语言,都避免不了一个特定的类型系统。哪怕是可随意改变变量类型的动态类型的语言,我们在读代码的过程中也需要脑补某个变量在运行时的类型。
如果每个类型传参都写一个重载方法,不符合复用的原则。
上个例子中,<T, K, V>就是对于泛型的运用,这里举一个伪代码的例子
拍电影
版本1
public void 电影(男主,女主) {
男主杀死了女主
}
这里类型绑定了,如果想写女主杀死男主就要创建一个新的重载函数
未来如果我想让男主杀死配角呢,在写一个重载函数太丑陋了我们可以选择对可以传入的角色做一个泛型<角色>男主女主配角都是,这里假设他们没有共同父类和接口
泛型解耦
版本2:
public void <T, K> 电影(T 角色1,K 角色2) {
角色1杀死了角色2
}
看看还有没可以优化的,牢记解耦控制和逻辑
剧情只有一个杀死太单调了,我想看更多的爱恨情仇怎么办
版本3:
public void <T, K, V> 电影(T 男主,K 女主,V 情节(A,B){角色1 爱/恨/情/仇 角色2}){
情节(男主,女主)
}
这样复用性很好了
补充思考
函数式是不是让你想起linux的shell命令 pipeline 模式
ps auwwx | awk '{print $2}' | sort -n | xargs echo
上面的例子是要查看一个用户执行的进程列表,列出来以后,然后取第二列,第二列是它的进程 ID,排个序,再把它显示出来。
程序伪代码可以这么写
xargs( echo, sort(n, awk('print $2', ps(auwwx))) )
我们也可以把函数放进数组里面,然后顺序执行一下。
可以尝试实现。