我们最常用的面向对象编程(Java)属于命令式编程(Imperative Programming)这种编程范式。常见的编程范式还有逻辑式编程(Logic Programming),函数式编程(Functional Programming)。
1.概述
1.1 面向对象编程
面向对象编程(OOP)则是一种模拟现实世界的方式。你可以把它想象成在电脑世界里创造出各种“物体”,这些物体有自己的特性(属性)和行为(方法)。
1.1.1 基本原理
OOP通过封装、继承和多态等原则来解决问题。这就像是给电脑世界里的物体分类,定义它们共同的行为,然后通过继承来创建更具体的子类。
1.1.2 优缺点
OOP的优点是它能帮助我们更好地组织和管理复杂的程序,让程序的结构更加清晰,比较适合大型工程中多人合作。但缺点是可能会导致过度设计,使得程序变得复杂难懂。这里展开谈几点:
优点:
- 提高了代码的可重用性:通过类来创建模块化的代码,而不需要每次都从头开始设计。
- 易于维护和修改:由于OOP的封装特性,代码的特定部分可以独立于其他部分进行修改,而不会影响到整个系统。
- 提高了代码的可读性:OOP倾向于创建有明确层次和关系的代码结构,这有助于新的开发者理解代码的工作方式。对象、类、继承等概念都是人类自然理解的,这就像是给代码建立了一个清晰的家谱。
缺点:
- 过度设计:开发者可能会被各种设计模式和原则所束缚,创建出许多不必要的抽象层和类,这反而使得程序难以理解和维护。这就像是为了建造一栋只需要简单功能的小屋,却设计了一个复杂的多层建筑。
- 性能问题:面向对象的程序可能因为抽象和封装导致性能上的开销。例如,对象创建、方法调用、继承等都可能增加额外的内存和CPU使用。
- 降低了代码的灵活性:在某些情况下,OOP的严格结构可能限制了代码的灵活性。一旦一个类的结构被定义,修改它可能会牵一发而动全身,尤其是在有很多继承和依赖的情况下。
1.1.3 面向对象编程示例
public class MainTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (int i = 0; i < list.size(); i++) {
Integer num = list.get(i);
list.set(i, num * num);
}
System.out.println(list);
}
}
1.2 函数式编程
函数式编程就像是在做数学题,你定义了一些函数,每个函数都只关心输入和输出,不会去改变外面的世界。这种范式强调无副作用的函数,避免了状态和可变数据。
1.2.1 基本原理
它通过高阶函数、纯函数和递归等概念来解决问题,就好比你用一套数学公式来描述问题的解决方案。
一切都是数学函数。函数式编程语言里也可以有对象,但通常这些对象都是恒定不变的 —— 要么是函数参数,要什么是函数返回值。
函数式编程语言里没有 for/next 循环,因为这些逻辑意味着有状态的改变。相替代的是,这种循环逻辑在函数式编程语言里是通过递归、把函数当成参数传递的方式实现的。
1.2.2 优缺点
函数式编程的优点是代码通常更简洁、更易于推理,但它的缺点是对于习惯了命令式编程的人来说,可能需要一些时间来适应。这里展开谈几点:
优点:
- 简洁的代码:函数式编程往往可以用更少的代码完成同样的功能。
- 易于推理:函数式编程强调无副作用的纯函数,这意味着给定相同的输入,函数总是产生相同的输出,不会影响或被外部状态影响。
- 更好的并发性:因为函数式编程避免使用可变状态,所以它天生适合并发编程。在多线程环境中,不需要担心线程安全问题,因为数据不会被多个线程同时修改。
缺点:
- 学习曲线:对于那些习惯了命令式编程的开发者来说,函数式编程的概念可能会显得陌生和难以理解,需要一些时间来适应不同的操作方式。
- 抽象层次高:函数式编程通常比命令式编程更抽象,这有时候会使得代码难以理解。特别是在处理非常复杂的逻辑时,过度的抽象可能会使得代码读起来像是在解谜。
- 性能问题:某些情况下,函数式编程可能会引入性能问题。例如,频繁的创建新对象(而不是修改现有的对象)可能会导致内存使用效率低下。递归,尽管是函数式编程中的常见模式,但如果不当使用,可能会导致栈溢出错误。
1.2.3 函数编程示例
public class MainTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// map函数做转化
List<Integer> results = list.stream().map(num -> num * num).collect(Collectors.toList());
System.out.println(results);
}
}
2.函数与方法
2.1 方法示例
我们先来看一个例子:
public class ShopTest {
static ShopManager smr = new ShopManager("王店长");
static class ShopManager {
String name;
public ShopManager(String name) {
this.name = name;
}
}
static String pay(String person) {
return (person + "买了商品后,向[" + smr.name + "]付款");
}
public static void main(String[] args) {
System.out.println(pay("张三"));
System.out.println(pay("张三"));
System.out.println(pay("张三"));
}
}
以上 pay 的执行结果,除了参数变化外,希望函数的执行规则永远不变
张三买了商品后,向[王店长]付款
张三买了商品后,向[王店长]付款
张三买了商品后,向[王店长]付款
然而,由于设计上的缺陷,函数引用了外界可变的数据,如果这么使用
smr.name = "李店长";
System.out.println(pay("张三"));
结果就会是
张三买了商品后,向[李店长]付款
这个在调用时,店长莫名被换掉了。问题出在哪儿呢?函数的目的是除了参数能变化,其它部分都要不变,这样才能成为规则的一部分。佛祖要成为规则的一部分,也要保持不变
static class ShopManager {
final String name;
public ShopManager(String name) {
this.name = name;
}
}
在 name
属性上添加了final
关键字,这样就在设定参数后不可再变化。
这里主要说明:
- 不是说函数不能引用外界的数据,而是它引用的数据必须也能作为规则的一部分
- 让 smr.name 不变,才能成为规则
2.2 方法与函数关系
方法本质上也是函数。不过方法绑定在对象之上,它是对象个人法则
方法是
- 对象数据.方法(其它参数)
函数是
- 函数(对象数据,其它参数)
2.3 函数转化函数对象
函数是无形的,也就是它代表的规则:它只强调入参的类型、个数以及返回结果,至于中间的怎么实现是不关心的。
若要函数有形,让函数的规则能够传播,需要将函数化为函数对象。
接下来对比下面的示例,看看函数对象到底指什么。
public class MyCalculate {
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
// OOP对象
MyCalculate calculate = new MyCalculate();
// 调用
System.out.println(calculate.add(1, 2));
}
}
与
public interface Calculate {
int calculate(int a, int b);
}
public class CalculateTest {
public static void main(String[] args) {
// 它已经变成了一个 函数对象
Calculate add = (a, b) -> a + b;
// 调用
System.out.println(add.calculate(1, 2));
// 规则可以由函数对象变化
Calculate subtract = (a, b) -> a - b;
System.out.println(subtract.calculate(10, 5));
}
}
可以看到,我们定义了一个函数 int calculate(int a, int b);
,并没有具体的实现。当我们使用时,在将其转换成具体的函数对象,由对象来具体实现细节。这个看起来就和匿名内部类中的方法使用好相似。
那方法与函数两者区别在哪?
- 前者是纯粹的一条两数加法规则,它的位置是固定的,要使用它,需要通过
MyCalculate对象.add
找到它,然后执行 - 而后者(
Calculate add
对象)就像长了腿,它的位置是可以变化的,想去哪里就去哪里,哪里要用到这条加法规则,把它传递过去 - 接口的目的是为了将来用它来执行函数对象,此接口中只能有一个方法定义
2.4 行为参数化
已知学生类定义如下
public class Student {
private String name;
private int age;
private String sex;
public Student(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String getSex() {
return sex;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
针对一组学生集合,筛选出男学生,下面的代码实现如何
public class StudentTest {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("张无忌", 18, "男"));
studentList.add(new Student("杨不悔", 16, "女"));
studentList.add(new Student("周芷若", 19, "女"));
studentList.add(new Student("宋青书", 20, "男"));
// 能得到 张无忌,宋青书
System.out.println(filter(studentList));
}
private static List<Student> filter(List<Student> students) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if ("男".equals(student.getSex())) {
result.add(student);
}
}
return result;
}
}
如果需求再变动一下,要求找到 18 岁以下的学生,上面代码显然不能用了,改动方法如下
static List<Student> filter(List<Student> students) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.getAge() <= 18) {
result.add(student);
}
}
return result;
}
// 能得到 张无忌,杨不悔
System.out.println(filter(students));
那么需求如果再要变动,找18岁以下男学生,怎么改?显然上述做法并不太好。更希望一个方法能处理各种情况,仔细观察以上两个方法,找不同。
不同在于筛选条件部分:
student.getSex().equals("男")
和
student.getAge() <= 18
既然它们就是不同,那么能否把它作为参数传递进来,这样处理起来不就一致了吗?
它俩要判断的逻辑不同,那这两处不同的逻辑必然要用函数来表示,将来这两个函数都需要用到 student 对象来判断,都应该返回一个 boolean 结果,怎么描述自定义函数的长相呢?
interface Lambda {
boolean test(Student student);
}
方法可以统一成下述代码
static List<Student> filter(List<Student> students, Lambda lambda) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (lambda.test(student)) {
result.add(student);
}
}
return result;
}
最后怎么给它传递不同实现呢?
// 找男学生
List<Student> results = filter(studentList, student -> student.getSex().equals("男"));
System.out.println(results);
// 找18岁以下的学生
results = filter(studentList, student -> student.getAge() <= 18);
System.out.println(results);
// 找18岁以下男学生
results = filter(studentList, student -> student.getSex().equals("男") && student.getAge() <= 18);
System.out.println(results);
这样就实现了以不变应万变,而变换即是一个个函数对象,也可以称之为行为参数化。
3.函数编程语法
在 Java 语言中,lambda 对象有两种形式:lambda 表达式与方法引用
- lambda 表达式: 功能更加全面
- 方法引用: 写法更加简洁
3.1 Lambda表达式
Lambda表达式是Java 8 引入的一种新的语法元素,它允许我们将函数作为方法参数传递,或者将代码作为数据处理。
Lambda表达式的基本语法如下:
(parameters) -> expression(parameters) -> { statements; }
parameters
:参数列表,可以为空或包含多个参数。->
:Lambda操作符,将参数列表与Lambda主体分隔开。expression
或{ statements; }
:Lambda主体,可以是一个表达式或一组语句。
示例:
(int a, int b) -> a + b;
明确指定参数类型
(int a, int b) ->{ int c = a + b; return c; }
多行执行语句需要{},若有返回值需要return
Lambda Lambda = (a, b) -> a + b;
public interface Lambda {
int op(int a, int b);
}
可以根据上下文推断出参数类型时,可以省略参数类型
a -> a;
只有一个参数时,可以省略 ()
lambda 对象的类型是由它的行为决定的,如果有一些 lambda 对象,它们的入参类型、返回值类型都一致,那么它们可以看作是同一类的 lambda 对象,它们的类型,用函数式接口来表示
与匿名内部类的对比
Lambda表达式可以看作是匿名内部类的简化形式。它们都可以用来创建函数式接口的实例。以下是Lambda表达式与匿名内部类的对比:
语法简洁性:Lambda表达式通常比匿名内部类更简洁。
类型推断:Lambda表达式允许编译器推断参数类型,而匿名内部类需要显式声明。
作用域:Lambda表达式可以捕获外部作用域的变量,但这些变量必须是final或实际上是final的。
在Java 8之前,我们可能会这样创建一个线程:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from a thread!");
}
}).start();
使用Lambda表达式,代码可以简化为:
new Thread(() -> System.out.println("Hello from a thread!")).start();
3.2 方法引用
将现有的方法调用转换为函数对象。
1)静态方法引用
静态方法引用允许我们直接引用一个类的静态方法。语法如下:
ClassName::staticMethodName
如何理解:
- 函数对象的逻辑部分是:调用此静态方法
- 因此这个静态方法需要什么参数,函数对象也提供相应的参数即可
例如,我们可以引用Integer类的parseInt静态方法:
List<String> numbers = Arrays.asList("1", "2", "3");
List<Integer> nums = numbers.stream().map(Integer::parseInt).collect(Collectors.toList());
这里的 Integer::parseInt
其实是 Integer 类中静态 parseInt(String s ) 方法的:
public final class Integer extends Number implements Comparable<Integer> {
...
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
}
...
}
2)实例方法引用
实例方法引用允许我们引用特定对象的实例方法。语法如下:
instanceReference::methodName
如何理解:
- 函数对象的逻辑部分是:调用此非静态方法
- 因此这个函数对象需要提供一个额外的对象参数,以便能够调用此非静态方法
- 非静态方法的剩余参数,与函数对象的剩余参数一一对应
例如,我们可以引用String对象的length方法:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
int maxLength = names.stream().map(String::length).max(Integer::compare).orElse(0);
这里的 String::length
其实是 (String str) -> str.length()
。表示需要 String的实例对象调用 length() 方法。
3)对象::非静态方法名
与实例方法引用类似,不过需要提供额外的对象。
如何理解:
- 函数对象的逻辑部分是:调用此非静态方法
- 因为对象已提供,所以不必作为函数对象参数的一部分
- 非静态方法的剩余参数,与函数对象的剩余参数一一对应
较为典型的一个应用就是 System.out
对象中的非静态方法,打印输出:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().forEach(System.out::println);
.forEach(System.out::println)
对应的表达式 (Object obj) -> System.out.println(obj);
4)构造器引用
构造器引用允许我们引用类的构造器。语法如下:
ClassName::new
函数类型应满足
- 参数部分与构造方法参数一致
- 返回值类型与构造方法所在类一致
例如,我们可以引用ArrayList的构造器:
// 构造无参
Supplier<List<String>> supplier = ArrayList::new;
List<String> list = supplier.get();
// 构造有参
Supplier<List<String>> supplier2 = () -> new ArrayList<>(5);
List<String> list2 = supplier2.get();
以上4种类型的方法引用,其实和平时的方法调用区别不大,简而言之,就是首先要知道入参是什么,然后你需要调用哪些类做什么业务逻辑。
写出等价的方法引用示例:
public class LambdaTest {
public static void main(String[] args) {
//Function<String, Integer> lambda1 = (String s) -> Integer.parseInt(s);
Function<String, Integer> lambda1 = Integer::parseInt;
//BiPredicate<List<String>, String> lambda2 = (list, element) -> list.contains(element);
BiPredicate<List<String>, String> lambda2 = List::contains;
//BiPredicate<Student, Object> lambda3 = (stu, obi) -> stu.equals(obi);
BiPredicate<Student, Object> lambda3 = Object::equals;
//Predicate<File> lambda4 = (file) -> file.exists();
Predicate<File> lambda4 = File::exists;
Runtime runtime = Runtime.getRuntime();
//Supplier<Long> lambda5 = () -> runtime.freeMemory();
Supplier<Long> lambda5 = runtime::freeMemory;
}
}
4.函数对象类型
4.1如何对函数对象类型分类
如何分类?
- 参数个数类型是否相同
- 返回值类型是否相同
函数式接口定义:
- 仅包含一个抽象方法
- 用
@Functionallnterface
来检查
示例:
public class LambdaTest {
public static void main(String[] args) {
Type1 type1 = s -> "Mr." + s;
System.out.println(type1.op("zhang"));
Type2 type2 = num -> num + 10;
System.out.println(type2.op(20));
}
@FunctionalInterface
public interface Type1 {
String op(String name);
}
@FunctionalInterface
public interface Type2 {
int op(int num);
}
}
上面演示了定义两个函数式接口 Type1
,Type2
。这两个参数类型和返回类型都不相同。那怎么使用一个函数式接口来同时满足上面的调用呢?
我们可以通过泛型:
@FunctionalInterface
public interface CommonType<P, R> {
R op(P param);
}
这样我们就可以使用相同的函数式接口了:
CommonType<String, String> commonType1 = s -> "Mr." + s;
CommonType<Integer, Integer> commonType2 = num -> num + 10;
CommonType<String, Integer> commonType3 = Integer::parseInt;
System.out.println(commonType1.op("zhang"));
System.out.println(commonType2.op(20));
System.out.println(commonType3.op("45"));
4.2 JDK中的函数对象类型
4.2.1 Runnable
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
4.2.2 Callable
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
4.2.3 Comparator
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
4.2.4 Consumer
消费者接口,接受T 返回void
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
4.2.5 Function
单元函数,Function<T,R> 接收一个T类型的参数,返回一个R类型对象
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
4.2.6 Predicate
谓词接口,接受一个T 返回boolean
有 Predicate<T> IntPredicate LongPredicate DoublePredicate
Predicate 除了有谓词的意思,还有断定,断言的意思,它的作用就是声明一个或组合的条件,满足为true,不满足为false
看接口中定义的方法, 接口方法中有一个test接口需要我们实现,其他都是默认方法,包括逻辑与,做逻辑非操作,逻辑或。
还有一个非基本类型(仅指int long daouble)Predicate没有的方法判断两个对象是否相同
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
4.2.7 UnaryOperator
单元操作接口 接受一个T类型参数 返回T类型对象
单元操作接口有 UnaryOperator LongUnaryOperator IntUnaryOperator DoubleUnaryOperator
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
使用示例:
我们需要对某个整数数组中元素判断是偶数,或者判断元素是完数的操作。
public class LambdaTest {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1, 2, 4, 5, 6, 7, 9, 10, 14);
List<Integer> results1 = filter(nums);
System.out.println(results1);
// 把规则抽象出来
List<Integer> results2 = filter(nums, num -> (num & 1) == 0);
System.out.println(results2);
List<Integer> results3 = filter(nums, LambdaTest::isPerfectNumber);
System.out.println(results3);
}
static List<Integer> filter(List<Integer> list) {
List<Integer> results = new ArrayList<>();
for (Integer num : list) {
// 规则需要变动
if ((num & 1) == 0) {
results.add(num);
}
}
return results;
}
static List<Integer> filter(List<Integer> list, Predicate<Integer> predicate) {
List<Integer> results = new ArrayList<>();
for (Integer num : list) {
if (predicate.test(num)) {
results.add(num);
}
}
return results;
}
public static boolean isPerfectNumber(int num) {
int sum = 0;
for (int i = 1; i < num; i++) {
if (num % i == 0) {
sum = sum + i;
}
}
if (sum == num) {
return true;
} else {
return false;
}
}
}
解读: 在第一个filter 方法中,需要把判断条件动态化,其类型为 boolean functionMethod(int num)
,这样我们就确定了是使用 Predicate的函数接口。
5. 闭包和变量捕获
1)理解闭包
在Java中,Lambda表达式可以访问并操作其外部作用域中的变量,这种现象称为闭包。闭包允许Lambda表达式捕获并存储对其外部作用域中变量的引用。
2)变量捕获的规则
Lambda表达式捕获外部变量时,必须遵守以下规则:
- 捕获的变量必须是final或实际上是final的(即,一旦初始化后就不能再被修改)。
- Lambda表达式内部不能修改捕获的变量。
例如,以下代码展示了变量捕获:
int baseNumber = 10;
UnaryOperator<Integer> addToBase = (Integer number) -> number + baseNumber;
// result will be 15
int result = addToBase.apply(5);
System.out.println(result);
for (int i = 0; i < 5; i++) {
//final int idx = i;
// 这里 idx 实际上是final
int idx = i;
Runnable runnable = () -> System.out.println(Thread.currentThread().getName() + ":" + idx);
new Thread(runnable).start();
}
6. 柯里化(Carrying)
柯里化的作用是让函数对象分步执行(本质上是利用多个函数对象和闭包),将多个接收参数的函数转换为接收一个参数的函数。
例如:
public class CarryingTest {
public static void main(String[] args) {
// 普通表达式
Cal cal = (a, b) -> a + b;
int exec = cal.exec(10, 20);
System.out.println(exec);
//柯里化,分步执行
highOrder(a -> b -> a + b);
}
static void highOrder(Step1 step1) {
Step2 step2 = step1.exec(10);
System.out.println(step2.exec(20));
}
interface Step1 {
Step2 exec(int a);
}
interface Step2 {
int exec(int b);
}
interface Cal {
int exec(int a, int b);
}
}
Step1是第一个函数对象,它的返回结果 Step2 是第二个函数对象
后者与前面的参数 a 构成了闭包
step1.exec(10) 确定了 a 的值是 10,返回第二个函数对象 step2,a 被放入了 step2 对象的背包记下来了
step2.exec(20) 确定了 b 的值是 20,此时可以执行 a + b 的操作,得到结果 30
评论区