1. Lambda表达式
1.1 Lambda表达式的前世–匿名类
-
匿名类定义:自从 JDK 1.1 于 1997 年发布以来,创建函数对象的主要手段就是匿名类。匿名类,通俗地讲,就是没有类名,直接通过new关键字创建这个类的实例。
-
匿名类特点:
- 匿名内部类的语法比较奇特,因为匿名内部类既是一个类的定义,同时它本身也是一个对象,因此从语法上看,它既有定义类的特征,也有创建对象的特征。
- 一般匿名对象只使用一次,而且匿名对象只在堆内存中开辟空间,而不存在栈内存的引用,所以此对象使用一次之后就等待被 GC(垃圾收集机制)回收。
-
匿名类最佳实践:匿名内部类,当作实参直接传递,间接高效。
public class InnerClassExercise01 { public static void main(String[] args) { // 当做实参直接传递,简洁高效 f1(new IL() { @Override public void show() { System.out.println("这是一副名画~~..."); } }); // 传统方法 f1(new Picture()); } // 静态方法,形参是接口类型 public static void f1(IL il) { il.show(); } } // 接口 interface IL { void show(); } // 类->实现IL => 编程领域 (硬编码) class Picture implements IL { @Override public void show() { System.out.println("这是一副名画XX..."); } }
1.2 Lambda表达式的今生
- 在 Java 8 中,语言形式化了这样的概念,使用单个抽象方法的接口)是特别的,应该得到特别的对待。 这些接口现在称为函数式接口,并且该语言允许你使用lambda 表达式或简称 lambda 来创建这些接口的实例。 Lambdas 在功能上与匿名类相似,但更为简洁。
- Lambda表达式起初是为了取代匿名内部类的使用,写出更优雅的代码,尤其是在集合的遍历和操作中,可以极大的简化代码的结构。Jdk也提供了大量的内置函数式接口供我们使用。
- Lambda表达式的本质:对于那些只有单个抽象方法的接口,如果想要快速获得该接口的实现类,那么这个实现类至少得实现这个抽象方法。Lambda表达式提供了一种语法糖,该语法糖重点关注于实现该抽象方法。
1.3 Lambda表达式的语法及使用条件
-
lambda 表达式的语法格式如下:
(parameters) -> expression 或 (parameters) ->{ statements; }
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。
-
Lambda表达式对接口的要求:
- 虽然lambda表达式可以对某些接口进行简单的实现,但是不是所有的接口都可以用lambda表达式来实现,Lambda表达式规定接口中只能有一个需要被实现的方法(抽象方法),但不是规定接口中只能有一个方法。其他方法可以使用default进行默认的实现,进而不影响Lambda表达式的使用。
@FunctionalInterface
注解:修饰函数式接口的,要求被修饰的接口抽象方法只能有一个,这个注解往往和lambda表达式一起使用。@FunctionalInterface
注解标识的作用主要是为了给编译器看,给编译器提供信息的,不管有没有这个注解,只要满足函数式的要求,那么它就是一个函数式接口。
1.4 Lambda表达式与匿名类比较
-
首先定义一个接口中的唯一抽象方法带参数的情况:
@FunctionalInterface public interface Learn { void study(); }
-
传统写法:
class Student implements Learn { @Override public void study() { System.out.println("我爱读书"); } } new Student().study(); //我爱读书
-
匿名类写法:
// 匿名类写法 new Learn() { @Override public void study() { System.out.println("我要进厂辣"); } }.study(); //我要进厂辣
-
Lambda表达式写法:
Learn learn2 = () -> { System.out.println("我想吃饭");}; earn2.study(); //我想吃饭
1.5 Lambda表达式具体案例
-
数组排序:
public class LambdaTest { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); list.add(11); list.add(22); list.add(33); list.add(44); System.out.println("排序前:" + list); //排序前:[11, 22, 33, 44] // 匿名内部类写法 Collections.sort(list, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); System.out.println("从小到大排序如下:"); list.forEach(integer -> System.out.println(integer)); //[11, 22, 33, 44] // Lambda写法 Collections.sort(list, (o1, o2) -> o2 - o1); System.out.println("从大到小排序如下:"); list.forEach(integer -> System.out.println(integer)); //[44, 33, 22, 11] }
2. 方法引用(Method Reference)
-
前言:Java 8引入了方法引用(method reference)作为一种语言特性,它可以简化代码,使得代码更加易读和易于维护。方法引用可以被视为Lambda表达式的简写形式,可以用来替代Lambda表达式中只调用一个已有方法的情况。总的来说该特性使得Java代码更加简洁和灵活。
-
使用条件:只可以替换单方法的Lambda表达式:
// 不可转化 Predicate<Integer> p2 = integer -> { System.out.println("你好"); return TestUtil.isBiggerThan3(integer); }; // 可以转化 Predicate<Integer> p2 = integer -> TestUtil.isBiggerThan3(integer);
-
使用场景:当使用方法引用替换Lambda表达式具有更好的可读性时,考虑使用。
-
简写规则:当抽象方法中的形参作为Lambda表达式中单方法的形参,可以省略这个形参。下述四个案例在该规则的基础上进行简写。
2.1 调用类的静态方法
-
定义:当实现函数式接口中的抽象方法时需要依靠一个静态方法。
-
lambda:
(args) -> Class.staticMethod(args)
-
method reference:
Class::staticMethod
-
实例:
-
函数式接口:Animal
@FunctionalInterface public interface Animal { // 抽象方法 String eat(); }
-
工具类:FoodUtil
public class FoodUtil { public static String giveFood() { return "全体起立,开始吃饭"; } }
-
静态方法引用测试类:StaticReference
public class StaticReference { public static void main(String[] args) { // lambda Animal animal1 = () -> FoodUtil.giveFood(); System.out.println(animal1.eat()); //全体起立,开始吃饭 // 方法引用 Animal animal2 = FoodUtil::giveFood; System.out.println(animal2.eat()); //全体起立,开始吃饭 } }
-
2.2 调用抽象方法的实例参数中的方法
-
定义:当实现函数式接口中的抽象方法的需要依靠抽象方法的某个实例参数中的方法。
-
lambda:
(obj, args) -> obj.instanceMethod(args)
-
method reference:
ObjectType::instanceMethod
-
实例:
-
函数式接口:Animal
@FunctionalInterface public interface Animal { String eat(Person person,Integer day); }
-
Person实体类:Person
public class Person { String name; Integer day; public Person() {} public Person(String name) { this.name = name; } public String getName(Integer day) { switch (day) { case 1 -> name = "小红"; case 2 -> name = "小黑"; case 3 -> name = "小黄"; default -> name = "小绿"; } return name; } }
-
实例参数的方法引用测试类:
public class ParameterReference { public static void main(String[] args) { // lambda Animal animal1 = (person, day) -> { return person.getName(day); }; // 今天给动物喂饭的人是:小红 System.out.println("今天给动物喂饭的人是:" + animal1.eat(new Person(), 1)); // 方法引用 Animal animal2 = Person::getName; // 今天给动物喂饭的人是:小黑 System.out.println("今天给动物喂饭的人是:" + animal2.eat(new Person(), 2)); } }
-
2.3 调用已存在的实例的方法
-
定义:当实现函数式接口中的抽象方法时需要依靠一个已存在的实例的方法。
-
lambda:
(args) -> obj.instanceMethod(args)
-
method reference:
obj::instanceMethod
-
实例:
-
函数式接口:Animal
@FunctionalInterface public interface Animal { String eat(String water); }
-
工具类:FoodUtil
public class FoodUtil { public String giveWater(String water) { return water; } }
-
调用已存在的实例的方法测试类:InstanceReference
public class InstanceReference { public static void main(String[] args) { FoodUtil foodUtil = new FoodUtil(); // lambda Animal animal1 = (water) -> foodUtil.giveWater(water); // 今天给动物喝:可乐 System.out.println("今天给动物喝:" + animal1.water("可乐")); // 方法引用 Animal animal2 = foodUtil::giveWater; // 今天给动物喝:雪碧 System.out.println("今天给动物喝:" + animal2.water("雪碧")); }
-
2.4 调用类的构造函数
-
定义: 当lambda中的单方法是调用某个类的构造函数,我们就可以将其写成如上形式的方法引用。
-
lambda:
(args) -> new ClassName(args)
-
method reference:
ClassName::new
-
实例:
-
函数式接口:Animal
@FunctionalInterface public interface Animal { Home Live(String address,Integer num); }
-
Home实体类:Home
public class Home { private String address; private Integer num; public Home() {} public Home(String address, Integer num) { this.address = address; this.num = num; } }
-
调用类的构造函数测试类:ConstructorReference
public class ConstructorReference { public static void main(String[] args) { // lambda Animal animal1 = (address, num) -> new Home(address,num); // 动物今天住在:com.mnnu.method_reference.Home@448139f0 System.out.println("动物今天住在:" + animal1.Live("一号场馆",101)); // 方法引用 Animal animal2 = Home::new; // 动物今天住在:com.mnnu.method_reference.Home@7ba4f24f System.out.println("动物今天住在:" + animal1.Live("二号场馆",202)); } }
-
3. 函数式接口
-
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
-
JDK 1.8 新增加的函数接口:
java.util.function
。java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口有:Java 8 函数式接口 | 菜鸟教程 (runoob.com)。其中常用的函数式接口为:Suppiler
、Consumer
、Predicate
、BiFunction
。
-
Suppiler:无参数,返回一个结果。
Supplier<Long> supplier = () -> System.currentTimeMillis(); System.out.println(supplier.get()); //1709457288028
-
Consumer:接受一个输入参数并且无返回的操作。
Consumer<String> consumer = s -> System.out.println(s.toUpperCase()); consumer.accept("hello"); //HELLO
-
Predicate:接受一个输入参数,返回一个布尔值结果。
Predicate<String> predicate = s -> s.contains("h"); System.out.println(predicate.test("hello")); //true
-
BiFunction<T,U,R>:代表了一个接受两个输入参数的方法,并且返回一个结果。
BiFunction<String,Integer, Car> biFunction = (name,age) -> new Car(name,age); System.out.println(biFunction.apply("宝马",2)); //Car{name='宝马', age=2}
4. Stream流
- Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
4.1 获取流
-
ArrayList<Person> arrayList = new ArrayList<>(10); arrayList.add(new Person("小红", 10)); arrayList.add(new Person("小绿", 20)); arrayList.add(new Person("小黄", 30)); arrayList.add(new Person("小黑", 25)); arrayList.add(new Person("小白", 17)); arrayList.add(new Person("小青", 19));
-
Collection中的stream方法。
Stream<Person> stream = arrayList.stream();
-
Stream.of方法。
Stream<Integer> integerStream = Stream.of(1, 2, 3); Stream<String> stringStream = Stream.of(new String[]{"你好", "很好"});
4.2 注意事项
-
Stream流只能被操作一次。
Stream<Person> stream = arrayList.stream(); System.out.println(stream.count()); //6 System.out.println(stream.count()); //Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
-
Stream方法返回的是最新的流。
Stream<Person> stream1 = arrayList.stream(); System.out.println(stream1); //java.util.stream.ReferencePipeline$Head@41629346 Stream<Person> stream2 = stream1.filter(person -> person.getName().contains("小")); System.out.println(stream2); //java.util.stream.ReferencePipeline$2@6d311334
-
Stream不调用终结方法,中间操作不会被执行,常用的终结方法有
foreach
、count
。
4.3 常用方法
4.3.1 Stream
-
返回结果如果是一个新的Stream流那么可以继续对该流进行操作。
-
filter:按条件过滤。
arrayList.stream().filter(person -> person.getName().contains("红")).forEach(System.out::println); //Person{name='小红', age=10} arrayList.stream().filter(person -> person.getAge() > 20).forEach(System.out::println); //Person{name='小黄', age=30} Person{name='小黑', age=25}
-
map:集合映射。
arrayList.stream().map(person -> person.getName() + "man").forEach(System.out::println); /*小红man 小绿man 小黄man 小黑man 小白man 小青man*/
-
skip:省略前几个。
arrayList.stream().skip(2).forEach(System.out::println); /*Person{name='小黄', age=30} Person{name='小黑', age=25} Person{name='小白', age=17} Person{name='小青', age=19}*/
-
limit:取前n个。
arrayList.stream().limit(2).forEach(System.out::println); /*Person{name='小红', age=10} Person{name='小绿', age=20}*/
4.3.2 Boolean
-
allMatch:全部满足条件。
System.out.println(arrayList.stream().allMatch(person -> person.getName().contains("小"))); //true System.out.println(arrayList.stream().allMatch(person -> person.getName().contains("红"))); //false
-
anyMatch:任意一个满足条件。
System.out.println(arrayList.stream().anyMatch(person -> person.getAge() > 10)); //true System.out.println(arrayList.stream().anyMatch(person -> person.getAge() > 65)); //false
-
noneMatch:全部不满足条件。
System.out.println(arrayList.stream().noneMatch(person -> person.getName().contains("大"))); //true System.out.println(arrayList.stream().noneMatch(person -> person.getName().contains("小"))); //false
4.3.3 迭代
-
reduce(原始数据,(a,b)):开始时将原始数据赋值给a,通过b迭代集合数据,将lambda方法体中的返回值赋值给a,最后将a返回。
-
求Integer集合中的最大值:
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5); System.out.println(integerStream.reduce(0, (a, b) -> a > b ? a : b)); //5 System.out.println(integerStream.reduce(0,(a,b) -> a + b)); //15
注意上述的两次reduce方法只有第一个会被执行,因为同一个流只能被操作一次。
4.4 收集数据
- Stream流可以将返回的数据存储到不同的容器中。
-
收集到集合:collect(Collectors.toCollection(ArrayList::new))
ArrayList<Person> collect = arrayList.stream().filter(person -> person.getAge() > 22).collect(Collectors.toCollection(ArrayList::new)); System.out.println(collect); //[Person{name='小黄', age=30}, Person{name='小黑', age=25}]
-
收集到数组:toArray(Person[]::new)
Person[] people = arrayList.stream().filter(person -> person.getAge() < 20).toArray(Person[]::new); System.out.println(Arrays.toString(people)); //[Person{name='小红', age=10}, Person{name='小白', age=17}, Person{name='小青', age=19}]
-
收集并分组:Collectors.groupingBy()
Map<String, List<Dog>> dogMap = dogs.stream().filter(dog -> dog.getName().contains("小")).collect(Collectors.groupingBy(dog -> dog.getSex().equals("男") ? "男" : "女")); Set<Map.Entry<String, List<Dog>>> entries = dogMap.entrySet(); Iterator<Map.Entry<String, List<Dog>>> iterator = entries.iterator(); while (iterator.hasNext()) { Map.Entry<String, List<Dog>> next = iterator.next(); System.out.println(next.getKey() + "-" + next.getValue()); } /* 女-[Dog{name='小红', sex='女'}, Dog{name='小黄', sex='女'}] 男-[Dog{name='小绿', sex='男'}, Dog{name='小蓝', sex='男'}]*/
5. 默认方法(default)和静态方法(static)
-
前言:在
Java8
之前的版本中,接口中只能声明常量和抽象方法,接口的实现类中必须实现接口中所有的抽象方法。而在Java8中,接口中可以声明默认方法和静态方法。 -
Java8之前的缺陷:当需要修改接口时候,需要修改全部实现该接口的类,没法在给接口添加新方法的同时不影响已有的实现
-
引入原因:默认方法和静态方法的作用是为了提供更加灵活和方便的接口设计。默认方法可以提供接口方法的默认实现,从而减少实现类的工作量。而静态方法可以为接口提供与接口相关的工具方法,这些方法可以直接通过接口名来调用,而不需要创建实现类的实例。
5.1 基本语法
-
默认方法的语法如下:
public interface MyInterface { default void myMethod() { // 默认方法的实现代码 } }
-
静态方法的语法如下:
public interface MyInterface { static void myStaticMethod() { // 静态方法的实现代码 } }
5.2 基本使用
-
自定义接口:Vehicle
public interface Vehicle { String drive(); default String power() { return "一级动力"; } static String oilConsumption() { return "一级油耗"; } }
-
实现类:BMW
public class BMW implements Vehicle { @Override public String drive() { return "开宝马"; } @Override public String power() { // 返回接口的默认实现 return Vehicle.super.power(); // 重写接口的默认实现 // return "二级动力"; } public static void main(String[] args) { BMW bmw = new BMW(); System.out.println(bmw.drive()); //开宝马 System.out.println(bmw.power()); //一级动力 || 二级动力 System.out.println(Vehicle.oilConsumption()); //一级油耗 } }
5.3 默认方法冲突
5.3.1 接口和父类的冲突
-
若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时,遵循如下的原则。
- 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
-
public interface Vehicle { default String power() { return "Vehicle:一级动力"; } }
-
public class Car { public String power(){ return "Car:一级动力"; } }
-
public class BMW extends Car implements Vehicle { @Override public String power() { // return Vehicle.super.power(); 显示调用接口 // return super.power(); 默认父类优先 // return "二级动力"; 重写覆盖 } public static void main(String[] args) { BMW bmw = new BMW(); // System.out.println(bmw.power()); //Vehicle:一级动力 // System.out.println(bmw.power()); //Car:一级动力 // System.out.println(bmw.power()); //二级动力 } }
5.3.2 接口与接口的冲突
-
接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法), 那么必须覆盖该方法来解决冲突。
-
public interface Vehicle { default String power() { return "Vehicle:一级动力"; } }
-
public interface Car { public String power(){ return "Car:一级动力"; } }
-
public class BMW implements Car,Vehicle { @Override public String power() { // return Vehicle.super.power(); 显示调用接口 // return Car.super.power(); 显示调用接口 } public static void main(String[] args) { BMW bmw = new BMW(); // System.out.println(bmw.power()); //Vehicle:一级动力 // System.out.println(bmw.power()); //Car:一级动力 } }
参考博客: