函数式编程
函数式编程 是 Java 8 引入的一个重要特性,它允许开发者以函数作为一等公民(first-class citizens)的方式编程,即函数可以作为参数传递给其他函数,也可以作为返回值。 这极大地提高了代码的可读性、可维护性和复用性。函数式编程的核心概念包括高阶函数、Lambda 表达式、函数式接口、流(Streams)和 Optional 类等。
函数式编程的核心是Lambda表达式,它是一种简洁的表示匿名方法的方式。Lambda表达式允许你以更简洁的方式传递代码块作为参数给方法,这是Java 8及以后版本中引入的一个重要特性,它主要用在实现只有一个抽象方法的接口(即函数式接口)时。
Lambda表达式只能替代函数式接口的匿名内部类,所以我们先了解函数式接口。
函数式接口
函数式接口是一个具有且仅有一个抽象方法的接口。 它可以包含默认方法和静态方法,但这些方法不会破坏接口作为函数式接口的性质,因为它们不是抽象方法。
函数式接口的特点:
- 单一抽象方法:只含有一个抽象方法
- Lambda表达式支持:可以使用Lambda表达式简洁地实现该接口
@FunctionalInterface
注解:推荐使用@FunctionalInterface
注解来标记一个接口为函数式接口。这有助于编译器检查接口是否符合函数式接口的规范,并在接口中错误地添加了第二个抽象方法时提供编译时错误。
例如,我们常用的Comparetor
接口就是函数式接口
Comparator
接口使用到@FunctionalInterface
注释compare
方法是Comparator
接口唯一的抽象方法,尽管存在其他方法,但都是静态方法和默认方法,不会影响函数式接口的性质
Lambda表达式
Lambda表达式的使用
Lambda表达式用于替代函数式接口的匿名内部类对象,从而让程序更简洁,可读性更好。
基本格式如下:
(被重写方法的参数列表) -> { 被重写方法的方法体代码
}
【举例演示Lambda表达式】
实现函数式接口:
- 使用匿名内部类:
interface ITest1 {void function();
}public class LambdaDemo1 {public static void main(String[] args) {ITest1 iTest1 = new ITest1() {@Overridepublic void function() {System.out.println("被重写的接口抽象方法...");}};}
}
- Lambda表达式:
interface ITest1 {void function();
}public class LambdaDemo1 {public static void main(String[] args) {ITest1 iTest1 = ()->{System.out.println("被重写的接口抽象方法...");};}
}
自定义一个学生类,创建一个学生类对象数组,使用comparator
比较器 和 Arrays.sort()
方法对其排序(要求:使用匿名内部类和Lambda表达式两种方式实现):
//自定义学生类
class Student {public String name;public int age;public double score;public Student(String name, int age, double score) {this.name = name;this.age = age;this.score = score;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +", score=" + score +'}';}
}public class LambdaDemo1 {//Lambda表达式public static void byLambda(Student[] student) {Student[] students = new Student[3];for (int i = 0; i < students.length; i++) {students[i] = student[i];}System.out.println("排序前:" + Arrays.toString(students));Arrays.sort(students, (Student o1, Student o2)->{return o1.age - o2.age;});System.out.println("排序后:" + Arrays.toString(students));}//匿名内部类public static void byInnerclass(Student[] student) {Student[] students = new Student[3];for (int i = 0; i < students.length; i++) {students[i] = student[i];}System.out.println("排序前:" + Arrays.toString(students));Arrays.sort(students, new Comparator<Student>() {@Overridepublic int compare(Student o1, Student o2) {return o1.age - o2.age;}});System.out.println("排序后:" + Arrays.toString(students));}public static void main(String[] args) {Student[] students = new Student[3];students[0] = new Student("张三", 19, 98.2);students[1] = new Student("李四", 21, 88.5);students[2] = new Student("王五", 18, 99.9);byInnerclass(students);System.out.println("=====================");byLambda(students);}
- 匿名内部类中重写的方法的 参数列表 对应 Lambda表达式
->
的左边; 方法体 对应 Lambda表达式->
的右边
Lambda表达式的省略
Lambda表达式可以进行化简,其规则如下:
- 参数类型可以省略
- 如果只有一个参数,参数类型省略的同时 “()” 也可以省略,但多个参数不能省略
- 如果Lambda表达式中只有一行代码,大括号可以不写,同时分号 “;” 也要省略;如果这行代码是 return语句,也必须去掉return
例如:
//化简前
Arrays.sort(students, (Student o1, Student o2)->{return o1.age - o2.age;});//化简后
Arrays.sort(students, (o1, o2)->o1.age - o2.age);
对于原Lambda表达式:(Student o1, Student o2)->{return o1.age - o2.age;}
,首先省略了参数类型,但由于不止一个参数所以小括号不能省略,方法体中只包含一条语句,可以省略大括号并同时省略分号,由于是return语句,省略return。
方法引用
Java中的方法引用也是Java 8引入的一个特性,它 提供了一种更简洁的方式来引用方法或Lambda表达式。 方法引用可以看作是Lambda表达式的一种特殊形式,其中Lambda表达式仅仅是调用了已存在的方法。使用方法引用可以使代码更加简洁易读。
所以,可以将方法引用理解为对Lambda表达式的再进一步化简,主要分为:
- 静态方法引用
- 实例方法引用
- 特定类型方法引用
- 构造方法引用
静态方法引用
静态方法引用:通过类名直接引用静态方法。
格式为:类名::静态方法
应用场景:如果某个Lambda表达式中只是通过类名调用了一个静态方法,并且->
前后参数的形式一致,就可以使用静态方法引用
【示例】
创建一个场景,一个学生类,实现一个静态方法,实现了根据年龄的排序规则,利用Arrays.sort(T[] a, Comparator<? super T> c)
方法排序时,Lambda表达式的方法体中仅调用这个静态方法,使用静态方法引用化简:
class Student {public String name;public int age;public double score;public Student(String name, int age, double score) {this.name = name;this.age = age;this.score = score;}//静态比较方法public static int compareByAge(Student s1, Student s2) {return s1.age - s2.age;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +", score=" + score +'}';}
}public class LambdaDemo1 {public static void byLambda(Student[] student) {Student[] students = new Student[3];for (int i = 0; i < students.length; i++) {students[i] = student[i];}System.out.println("===== Lambda表达式 =====");System.out.println("排序前:" + Arrays.toString(students));Arrays.sort(students, (o1, o2)->Student.compareByAge(o1, o2));System.out.println("排序后:" + Arrays.toString(students));}public static void byInnerclass(Student[] student) {Student[] students = new Student[3];for (int i = 0; i < students.length; i++) {students[i] = student[i];}System.out.println("===== 匿名内部类 =====");System.out.println("排序前:" + Arrays.toString(students));Arrays.sort(students, new Comparator<Student>() {@Overridepublic int compare(Student o1, Student o2) {return Student.compareByAge(o1, o2);}});System.out.println("排序后:" + Arrays.toString(students));}public static void byMethodReference(Student[] student) {Student[] students = new Student[3];for (int i = 0; i < students.length; i++) {students[i] = student[i];}System.out.println("===== 静态方法引用 =====");System.out.println("排序前:" + Arrays.toString(students));Arrays.sort(students, Student::compareByAge);System.out.println("排序后:" + Arrays.toString(students));}public static void main(String[] args) {Student[] students = new Student[3];students[0] = new Student("张三", 19, 98.2);students[1] = new Student("李四", 21, 88.5);students[2] = new Student("王五", 18, 99.9);byInnerclass(students);byLambda(students);byMethodReference(students);}
}
实例方法引用
静态方法引用:通过实例对象直接引用实例方法。
格式为:对象名::实例方法
应用场景:如果某个Lambda表达式中只是通过对象名调用了一个实例方法,并且->
前后参数的形式一致,就可以使用实例方法引用
【示例】
相似的业务场景,只不过将静态方法引用中调用静态方法改为调用一个实例方法,使之成为实例方法引用的场景。
特定类型方法引用
特定类型的方法引用:使用类名来引用该类中任意对象的实例方法,此时Lambda表达式的第一个参数会是该方法的调用者。
格式为:特定类的名称::方法
应用场景:如果某个Lambda表达式里只是调用一个特定类型的实例方法,并且前面的参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参的,此时就可以使用特定类型的方法引用。
【示例】
现有一个字符串数组,要对该数组按字母顺序进行排序,排序时忽略大小写,即"Zh"应该在"ang"的后面:
public static void main(String[] args) {String[] strings = new String[]{"Zh","ang","Job","Aa","bb","Oo"};//正常排序,不能实现需求String[] strings1 = Arrays.copyOf(strings,strings.length);System.out.println("排序前:");System.out.println(Arrays.toString(strings1));Arrays.sort(strings1);System.out.println("排序后:");System.out.println(Arrays.toString(strings1));//忽略大小写排序String[] strings2 = Arrays.copyOf(strings,strings.length);System.out.println("排序前:");System.out.println(Arrays.toString(strings2));Arrays.sort(strings2, new Comparator<String>() {@Overridepublic int compare(String o1, String o2) {return o1.compareToIgnoreCase(o2);}});System.out.println("排序后:");System.out.println(Arrays.toString(strings2));//Lambda表达式String[] strings3 = Arrays.copyOf(strings,strings.length);System.out.println("排序前:");System.out.println(Arrays.toString(strings3));Arrays.sort(strings3, (o1, o2)->o1.compareToIgnoreCase(o2));System.out.println("排序后:");System.out.println(Arrays.toString(strings3));//特定方法引用String[] strings4 = Arrays.copyOf(strings,strings.length);System.out.println("排序前:");System.out.println(Arrays.toString(strings4));Arrays.sort(strings4, String::compareToIgnoreCase);System.out.println("排序后:");System.out.println(Arrays.toString(strings4));}
构造方法引用
构造方法引用:使用类名来引用构造方法
格式为:类名::new
应用场景:如果某个Lambda表达式里只是在创建对象,并且->
前后参数情况一致,此时就可以使用构造方法引用
【示例】
以下三种方法等价:
public static void main(String[] args) {//匿名内部类ICarFactory carFactory1 = new ICarFactory() {@Overridepublic Car createOb(String name, double price, String brand) {return new Car(name, price, brand);}};//Lambda表达式ICarFactory carFactory2 = (name, price, brand) -> new Car(name,price,brand);//构造方法引用ICarFactory carFactory3 = Car::new;}
Stream流
Stream是JDK8也是引入的新特性,是一套全新的API(java.util.stream.*),是函数式编程的重要部分,可以用于操作集合或者数组的数据。Stream流大量结合了Lambda表达式的语法风格,能够简化代码,增强代码可读性。
Stream流的使用步骤:
- 获取Stream流
- 调用中间方法,处理流上的数据
- 调用终止方法
获取Stream流
Stream
本身是一个接口,但集合(如 List
、Set
等)和数组可以通过特定的方法或操作来“使用”它,即生成 Stream
实例。
- 获取集合(Collection下) 的Stream流
Collection提供 | 解释 |
---|---|
default Stream stream() | 获取当前集合对象的Stream流 |
Collection
底下的集合类可以直接通过对象实例调用stream
方法,但Map
底下的不可以。
Map
相关集合类需要先调用values()
、keySet()
、entrySet()
三种方法,再调用stream()
方法分别获取值流、键流、键值对流。
示例:
public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.stream();Set<Integer> set = new HashSet<>();set.stream();Map<String,Integer> map = new HashMap<>();map.values().stream();map.keySet().stream();map.entrySet().stream();}
- 获取数组的Stream流
对于数组,Java 8 并没有在数组类型上直接添加 stream()
方法,因为数组是基本类型或者是固定大小的对象集合,并不直接实现 Collection
接口,但可以通过以下两种方法获取数组的Stream流:
Arrays类提供的方法 | 解释 |
---|---|
public static Stream stream(T[] array) | 获取当前数组的Stream流 |
Stream提供的方法 | 解释 |
---|---|
public static Stream of(T… values) | 获取当前接收数据的Stream流 |
of
方法的参数T...values
是可变参数
示例:
public static void main(String[] args) {String[] strings = new String[10];Arrays.stream(strings);Stream.of(strings);}
补充:可变参数
可变参数(也称为可变长参数或不定参数)是一种允许你传递任意数量参数的方法。这个功能通过 ...
语法在方法定义中指定。使用可变参数,你可以调用方法时传递零个或多个指定类型的参数,而不需要使用数组。这些参数在方法内部被当作一个数组处理。
格式:数据类型...参数名称
可变参数可以接收一个或多个数据,也可以直接接收数组,甚至可以不传入任何数据。
注意:
- 一个参数列表只能有一个可变参数
- 可变参数必须在参数列表的最后面
- 可变参数在方法内部就是一个数组
- 如果方法之间存在参数数量不同的重载版本,并且其中一个版本使用了可变参数,那么编译器可能会报告冲突。这是因为编译器可能会将具有固定数量参数的方法调用解释为对可变参数方法的调用。
示例:
public static void varargs(int a, int...b) {System.out.println("可变参数测试:");System.out.println(a);System.out.println(Arrays.toString(b));}public static void main(String[] args) {int[] array = new int[]{1, 2, 3, 4, 5};//不向可变参数传参varargs(1);//向可变参数传入一个数据varargs(1, 2);//向可变参数传入多个数据varargs(1, 2,3,4,5);//向可变参数传入数组varargs(1, array);}
中间方法
中间方法指的是调用完成后会返回新的Stream流,可以继续使用,即支持链式编程,可以连续使用多个中间方法。
可以将Stream流看作是一个传送带,集合或数组的数据都倒到传送带上,再由中间方法对数据进行筛选。
常用方法:
Stream提供的方法 | 说明 |
---|---|
Stream filter(Predicate<? super T> predicate) | 对流中数据进行过滤 |
Stream sorted() | 对元素进行升序排序,T 必须可比较 |
Stream sorted(Comparator<? super T> comparator) | 按照指定规则排序 |
Stream limit(long maxSize) | 获取前maxSize个元素 |
Stream skip(long n) | 跳过前n个元素 |
Stream distinct(); | 去重 |
Stream map(Function<? super T, ? extends R> mapper) | 加工/映射,将流上数据加工后变成新数据再放回流中 |
public static Stream concat(Stream<? extends T> a, Stream<? extends T> b) | 将a流和b流合并为一个流 |
示例:
public static void main(String[] args) {List<Integer> list = new ArrayList<>();Collections.addAll(list, 78,36,88,98,39,56,41,66,78);//filter-过滤出60+的数据list.stream().filter(s -> s >=60).forEach(System.out::println);//sorted-升序排序list.stream().sorted().forEach(System.out::println);//sorted-指定降序排序list.stream().sorted((o1, o2) -> o2 - o1).forEach(System.out::println);//limit-获取前3大的数据list.stream().sorted((o1, o2) -> o2 - o1).limit(3).forEach(System.out::println);//skip-跳过前3大的元素list.stream().sorted((o1, o2) -> o2 - o1).skip(3).forEach(System.out::println);//distinct-去重list.stream().distinct().forEach(System.out::println);//map-将所有数据-10list.stream().map(s -> s - 10).forEach(System.out::println);//concat-合并流List<String> list1 = new ArrayList<>();Collections.addAll(list1, "a","b","c");Stream.concat(list.stream(), list1.stream()).forEach(System.out::println);}
终止方法
终止方法指的是调用完成后,不返回新的Stream流,即无法再继续使用流。
当一个流调用了终止方法后,就不能再使用该流了,否则会抛出IllegalStateException
异常!
常用方法:
Stream提供的方法 | 解释 |
---|---|
void forEach(Consumer<? super T> action) | 对流运算后的元素执行遍历操作 |
long count() | 统计流运算后的元素个数 |
Optional max(Comparator<? super T> comparator) | 获取流运算后的最大值元素 |
Optional min(Comparator<? super T> comparator) | 获取流运算后的最小值元素 |
Optional
类是 Java 8 引入的一个容器类,用于包含非空值。Optional
类的引入主要是为了解决空指针异常(NullPointerException
)的问题,提供了一种更好的方式来处理可能为null
的情况。
Optional
类是一个可以包含也可以不包含非null
值的容器对象。如果值存在,isPresent()
方法将返回true
,调用get()
方法将返回该对象。
终止方法中有一种收集方法,能将Stream流操作后的结果转回到集合或者数组中。
开发中将数据收集到集合或数组中才会进一步使用,Stream流只是方便操作集合和数组的手段。
方法 | 解释 |
---|---|
<R, A> R collect(Collector<? super T, A, R> collector) | 将流处理后的结果集收集到一个指定的集合中 |
Object[] toArray() | 将流处理后的结果集收集到一个数组中 |
关于collect
方法的参数,要关注Collectors
工具类提供的具体的收集方式:
方法 | 解释 |
---|---|
public static Collector<T, ?, List> toList() | 把元素收集到List集合 |
public static Collector<T, ?, Set> toSet() | 把元素收集到Set集合 |
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) | 把元素收集到Map集合 |
这三个方法的返回值和参数较为复杂,搭配collect
方法使用:
public static void main(String[] args) {List<Integer> list1 = new ArrayList<>();Collections.addAll(list1, 78,36,88,98,39,56,41,66,78);List<Student> list2 = new ArrayList<>();Collections.addAll(list2, new Student("张三",12), new Student("李四",24 ),new Student("王五",18));//toListList<Integer> newList = list1.stream().sorted().collect(Collectors.toList());System.out.println(newList);//toSetSet<Integer> set = list1.stream().collect(Collectors.toSet());System.out.println(set);//toMapMap<String,Integer> map = list2.stream().collect(Collectors.toMap(s -> s.name, s -> s.age));System.out.println(map);//toArrayObject[] array = list1.stream().sorted().toArray();System.out.println(Arrays.toString(array));}
最后小结一下函数式编程的优势:
- 代码简洁:Lambda 表达式和 Stream API 可以让代码更简洁、更易读。
- 并行处理:Stream API 支持并行操作,可以自动利用多核处理器进行并行计算。
- 易于测试:函数式编程倾向于使用小的、独立的函数,这使得测试更加简单。
- 无副作用:函数式编程鼓励编写没有副作用(即不修改外部状态)的函数,这有助于保持代码的清晰和可预测性。
完