Java 8 作为一个里程碑式的版本,其中所做出的改变,在许多方面比Java历史上任何一次改变都深远。Java为什么会一直在改变,因为编程语言就像生态系统一样,更优秀语言的出现,落后的语言就会被取代,除非它们不断地演变和进步。Java 8 引入的核心新特性包括:Lambda表达式、函数式接口、Stream流API、方法引用/构造器引用等。
1. 什么是行为参数化
行为参数化是Java 8 增加的一个编程概念,即把不同行为的代码通过参数传递给方法。对于初学者来说这可能不太好理解,我们需要记住的是Java 8 增加了把方法(你的代码)作为参数传递给另一个方法的能力。Java 8中的Lambda表达式和方法引用的语法就是使用了行为参数化的体现。
- Lambda表达式,通过匿名函数传递代码。
package com.mj.lambda;/*** @author: gjm* @description: 常见的Lambda使用*/
public class UseLambda {public static void main(String[] args) {// Lambda表达式把一个匿名函数传递给方法new Thread(()-> System.out.println("t1 running"),"t1").start();}
}
- 方法引用语法,表示传递此方法的代码。
package com.mj.method_ref;import java.util.function.Function;/*** @author: gjm* @description: 如何使用方法引用*/
public class UseMethodRef {public static void main(String[] args) {// 方法引用语法: (类::方法名),传递的是方法的代码getThreadInfo(new Thread(()->System.out.println("t1 running"),"t1"),Thread::getId,Thread::getName);}// 使用函数式接口接收public static void getThreadInfo(Thread t, Function<Thread, Long> f, Function<Thread, String> f1) {// 调用函数,传递的方法不同,获取的结果也不同Long threadId = f.apply(t);String threadName = f1.apply(t);System.out.println("threadId:"+threadId);System.out.println("threadName:"+threadName);}}
2. 方法引用的语法
方法引用是Java 8 支持的新语法,使用 ": :" 的语法声明方法引用,可以引用静态方法、普通成员方法、构造器方法。
- 语法一:使用类: :普通成员方法。
package com.mj.method_ref;import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;/*** @author: gjm* @description: 用户类*/
@Setter
@Getter
@AllArgsConstructor
@ToString
public class User {private long id;private String username;private int age;
}
public static void main(String[] args) {List<User> users = new ArrayList<>();users.add(new User(1L,"张三", 30));users.add(new User(2L,"李四", 25));users.add(new User(3L,"王五", 35));// 可以按照语法理解,通过年龄从小到大排序,users.sort(Comparator.comparing(User::getAge));System.out.println(users.toString());}
- 语法二:使用类: :静态成员方法。
package com.mj.method_ref;import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;/*** @author: gjm* @description: 用户类*/
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {private long id;private String username;private int age;// 按用户年龄降序排序public static int compareByAgeDesc(User u1, User u2){return u2.getAge() - u1.getAge();}
}
public static void main(String[] args) {List<User> users = new ArrayList<>();users.add(new User(1L,"张三", 30));users.add(new User(2L,"李四", 25));users.add(new User(3L,"王五", 35));// 使用工具类的排序方法,引用自定义排序规则的静态方法Collections.sort(users, User::compareByAgeDesc);System.out.println(users.toString());}
- 语法三:对象: :普通成员方法。
public int compareByAgeAsc(User u1, User u2){return u1.getAge() - u2.getAge();}
public static void main(String[] args) {List<User> users = new ArrayList<>();users.add(new User(1L,"张三", 30));users.add(new User(2L,"李四", 25));users.add(new User(3L,"王五", 35));// 使用工具类的排序方法,引用自定义排序规则的静态方法User user = new User();Collections.sort(users, user::compareByAgeAsc);System.out.println(users.toString());}
- 语法四:构造器引用,类: :new。
package com.mj.method_ref;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;/*** @author: gjm* @description: 构造器引用*/
public class ConstructorRef {public static void main(String[] args) {List<String> names = new ArrayList<>();Collections.addAll(names,"11223","122","1222","12","21122","12aa");// 找到所有以12开头的字符串,并转换为数字List<Integer> integers = names.stream().filter(num -> {// 字符串只能是数字Pattern pattern = Pattern.compile("[0-9]+");return (pattern.matcher(num).matches() && num.startsWith("12"));} )// 把过滤出的数字字符串转换成 Integer类型,// Integer::new所对应构造器为 public Integer(String s).map(Integer::new).collect(Collectors.toList());System.out.println(integers.toString());}}
3. Lambda表达式
Lambda表达式也是Java 8 支持的新语法,本质就是一个匿名函数,把函数当成参数传递给其它方法。前面的方法引用使用的是具体方法,也就是需要被提前声明出的静态方法或普通成员方法才能被引用。
- 一个Lambda表达式包括了三个部分。
1. 参数列表: 和函数一样支持无参(),一个参数(a),多个参数(a, b , ..),无需书写参数类型。
2. 箭头: 箭头 —>把参数列表与Lambda主体分隔开。
3. Lambda主体: 在大括号{ }中书写具体的执行代码,只有一行代码可以省略大括号。
- Lambda表达式需匹配函数式接口才能使用。函数式接口就是只定义一个抽象方法的接口,具体说来,Lambda表达式是函数式接口一个具体实现的实例。
- Lambda表达式主体内可以使用外部的变量,外部变量必须是最终变量或使用final修饰的实际最终变量。
- 无参的Lambda表达式,可以有返回值和无返回值。
// 无参无返回值,对应的是函数式接口Runnable Runnable runnable = () -> { };// 无参有返回值,对应的是函数式接口Supplier<T>或者Callable<T>Supplier<String> supplier = () -> "Gets a result";// Callable一般用于多线程,可以拿到执行结果Callable<String> callable = () -> "Computes a result";
- 一个参数的Lambda表达式,可以有返回值和无返回值。
// 一个参数无返回值,对应的是函数式接口Consumer<T>
Consumer<String> consumer = (a) -> { };// 一个参数有返回值,对应的是函数式接口Function<T, R>
Function<Integer, String> f = (a) -> a.toString();
- 两个参数的Lambda表达式,可以有返回值和无返回值。
// 两个参数无返回值,对应的是函数式接口BiConsumer<T, U>
BiConsumer<String, Integer> biConsumer = (a, b) -> { };// 两个参数有返回值,返回类型为int,对应的是函数式接口Comparator<T>
Comparator<User> comparator = (u1, u2) -> u1.getAge() - u2.getAge();
4. 函数式接口
函数式接口也是Java 8 新增加的一种类型,函数式接口的定义是只有一个抽象方法的接口。函数式接口作为方法的参数时,可以使用Lambda表达式或者方法引用把代码作为参数值传递,这就是行为参数化。让方法通过接收多种不同的代码实现,来完成不同的行为,这也被称为函数式编程。
- Predicate函数式接口。
java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。我们可以使用一个具体的案例体验函数式编程的使用和强大之处,下面案例通过lambda表达式传入不同行为的代码,对过滤用户的方法完成了不一样的功能。这些代码必须匹配Predicate接口的抽象方法的定义规则,官方叫匹配函数式接口的函数描述符。
package com.mj.lambda;import com.mj.method_ref.User;import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;/*** @author: gjm* @description: Predicate函数式接口使用*/
public class UsePredicate {public static void main(String[] args) {List<User> users = new ArrayList<>();users.add(new User(1L,"张三", 30));users.add(new User(2L,"李四", 25));users.add(new User(3L,"王五", 35));/*** filterUsers()方法的第二个参数可以使用Lambda表达式* Lambda表达式必须匹配函数描述符: T-> boolean* 函数描述符: T-> boolean,表示接收一个T类型参数,返回boolean类型结果*/// 找出大于等于35岁的用户System.out.println(filterUsers(users, user -> user.getAge() >=30 ));// 找出姓张的用户System.out.println(filterUsers(users, user -> user.getUsername().startsWith("张") ));}/*** 过滤用户* @param user* @param p* @return*/public static List<User> filterUsers(List<User> users, Predicate<User> p) {List<User> result = new ArrayList<>();for (User user: users){// 执行方法,就是执行了lambda表达式中的代码if (p.test(user)) {result.add(user);}}return result;}
}
- Consumer函数式接口。
java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。由于只有输入参数,没有返回值,所以通常也叫消费型函数式接口。
package com.mj.lambda;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** @author: gjm* @description: Consumer函数式接口*/
public class UseConsumer {public static void main(String[] args) {List<String> names = new ArrayList<>();Collections.addAll(names,"刘备","关羽","张飞");/*** forEach()方法的参数就是Consumer函数式接口* default void forEach(Consumer<? super T> action) {}* forEach()方法也是jdk1.8新增加的接口中允许有实现的默认方法*/names.forEach(str -> System.out.println(str));}}
- Supplier函数式接口。
java.util.function.Supplier<T>接口定义了一个叫作get的方法,它不接收任何参数,只返回一个泛型T的对象。根据这个特性,通常也被称供给型函数式接口。
package com.mj.lambda;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.util.function.Supplier;/*** @author: gjm* @description: Supplier函数式接口*/
public class UseSupplier {public static void main(String[] args) {// 获取当前时间的字符串String currentDateTime = getDateTimeStr(LocalDateTime::now, "yyyy-MM-dd HH:mm:ss");System.out.println(currentDateTime);// 获取指定时间的字符串String dateTime = getDateTimeStr(()->LocalDateTime.of(2022, Month.APRIL,1,12,30),"yyyy-MM-dd HH:mm:ss");System.out.println(dateTime);}/*** 获取LocalDateTime时间的字符串格式* @param supplier* @param pattern* @return*/public static String getDateTimeStr(Supplier<LocalDateTime> supplier,String pattern){LocalDateTime time = supplier.get();return time.format(DateTimeFormatter.ofPattern(pattern));}
}
- Function函数式接口。
java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。根据这个接口的方法特点,我们可以利用它实现类型转换的功能。
package com.mj.lambda;import com.mj.method_ref.User;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;/*** @author: gjm* @description: Function函数式接口*/
public class UseFunction {public static void main(String[] args) {List<User> users = new ArrayList<>();users.add(new User(1L,"张三", 30));users.add(new User(2L,"李四", 25));users.add(new User(3L,"王五", 35));List<Map> dataMap = map(users, user -> {// 转换为HashMap集合,以id为keyMap<Long, String> map = new HashMap<>();map.put(user.getId(),user.getUsername());return map;});System.out.println(dataMap.toString());}/*** 把T类型的对象转换为R类型* @param list* @param f* @param <T>* @param <R>* @return*/public static <T, R> List<R> map(List<T> list, Function<T, R> f) {List<R> result = new ArrayList<>();for(T s: list){result.add(f.apply(s));}return result;}}
- 原始类型特化的函数式接口。
Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。这些函数式接口对原始类型进行了扩展,比如我们使用Predicate时需要指定输入参数的泛型T,如果输入的参数类型是可以确定的,比如int、long、double,那么我们就可以使用IntPredicate、LongPredicate、DoublePredicate等接口,这样可以避免使用包装类型,产生自动装箱操作。
package com.mj.lambda;import java.util.function.IntPredicate;
import java.util.function.Predicate;/*** @author: gjm* @description: IntPredicate*/
public class UseIntPredicate {public static void main(String[] args) {IntPredicate intPredicate = (int a) -> a==0;intPredicate.test(0); // 此时无自动装箱Predicate<Integer> predicate = (Integer a) -> a==0;predicate.test(0); // 产生了自动装箱} }
- 自定义函数式接口。
某些时候我们在java.util.function包下没找到符合自己要求的函数式接口时,我们可以自定义满足自己要求的函数式接口。根据函数式接口只有一个抽象方法的定义,我们也可以实现自定义函数式接口,从而来满足自己的开发。注意函数式接口上的@FunctionalInterface注解是非必需的,只是用来标识这是一个函数式接口,我们建议加上让代码更容易理解。
package com.mj.lambda;import java.lang.FunctionalInterface;/*** @author: gjm* @description: 自定义函数式接口*/
@FunctionalInterface
public interface CustomFunInterface<T,U,X,R> {R compute(T t, U u, X x);
}
package com.mj.lambda;/*** @author: gjm* @description: 测试自定义函数式接口*/
public class UseCustomFunInterface {public static void main(String[] args) {Integer a = 90, b = 20, c = 30;// 计算出a,b,c的最大值,并返回StringCustomFunInterface<Integer, Integer, Integer, String> myFunInterface = (x, y, z)-> {Integer max = x;max = max > y ? max : y;max = max > z ? max : z;return max.toString();};String compute = myFunInterface.compute(a, b, c);System.out.println(compute);}}
4. 什么是Stream流
Stream流是Java 8 提供的新API,它主要是用来帮助我们简单和高效地处理集合数据的,Stream提供了集合处理的常用API。我们可以使用Stream的API配合前面学的函数式编程思想实现更简洁的声明式方法表达和几乎透明的并行处理。
Java8之前如果我们想在集合中过滤掉并返回我们想要的数据,我们要对集合进行遍历,并书写自定义过滤逻辑处理再返回。这在代码层面往往不能够清晰地表达你所做的事情,Java8为我们提供了专门做这个事情的filter方法。
List<Employee> employees = new ArrayList<>();employees.add(new Employee(1L,"张三", 30));employees.add(new Employee(2L,"李四", 25));employees.add(new Employee(3L,"王五", 35));List<Employee> old = new ArrayList<>();// Java8以前的写法for(Employee e: employees){if(e.getAge() >= 35) old.add(e);}// 使用Stream API的写法old = employeeList.stream().filter(employee -> employee.getAge() >=35).collect(toList());
- Stream的filter方法,通过名称就能知道它的作用就是用于过滤数据,它只有一个参数并且类型是Predicate<? super T> 的函数式接口,返回值为Stream<T>,意味着它可以继续调用其它方法处理更复杂的逻辑,就像流一样把结果流向下一个方法处理。
List<Employee> employees = new ArrayList<>();employees.add(new Employee(1L,"张华", 35));employees.add(new Employee(2L,"王五", 35));employees.add(new Employee(3L,"张小樊", 21));employees.add(new Employee(4L,"李东东", 25));// 找到所有姓张的员工,并按年龄从小到大排序List<Employee> search = employeeList.stream().filter(employee -> employee.getName().startsWith("张")).sorted(Comparator.comparing(Employee::getAge)).collect(toList());
- Stream的map方法,map可以翻译为映射。这个名称比较有意思,我们可以这样理解,对于集合中的每一个元素,都可以通过一个函数的处理得到一个新的元素,这就叫映射。本质就是对流中每一个元素应用函数,使其做一种转换。它的方法参数是一个函数型的函数式接口Function<? super T, ? extends R>,这也体现了它适合做类型转换的特点。
List<RoleRefResource> roleRefResources = new ArrayList<>();roleRefResources.add(new RoleRefResource(1L,"/api/employee","roleA"));roleRefResources.add(new RoleRefResource(2L,"/api/department","roleB"));roleRefResources.add(new RoleRefResource(3L,"/api/user","roleC"));roleRefResources.add(new RoleRefResource(4L,"/api/employee","roleD"));String resPath = "/api/employee";// 获取可访问该资源路径的所有角色列表List<String> roles = roleRefResources.stream()// 过滤路径.filter(rr -> resPath.equals(rr.getResPath()))// 映射为角色名称.map(rr -> rr.getRoleName() ).collect(toList());
- Stream的flatMap方法。这个方法主要用来做流的扁平化处理,我们可以理解为把每一个处理结果所返回的流都合并为一个流。它和map的区别是多了一步flat扁平化的操作,我们通常可以利用flatMap来做一些合并操作。
Integer[] a = new Integer[]{1,3,5,2};Integer[] b = new Integer[]{1,4,5,9};List<Integer[]> list = Arrays.asList(a,b);// 把a,b集合合并成一个新集合,并过滤重复元素List<Integer> uniqueInteger = list.stream().flatMap(x -> Arrays.stream(x)) // 把每个集合都转换成流,并且合并.distinct() // 对合并的这个流过滤重复元素.sorted().collect(Collectors.toList());System.out.println(uniqueInteger);
- java.util.stream.Stream中的Stream接口定义了许多流操作(方法)。Stream的流操作主要分为两类:中间操作和终端操作,其中可以连接起来的流操作称为中间操作,关闭流的操作则称为终端操作。filter或sorted等中间操作会返回另一个流,返回的是流意味着它能继续执行其它流操作,就像工厂流水线一样分工执行。终端操作会从流的流水线生成结果,这返回的结果不再是任何的流而是Java中的各种类型List、Integer,或者是void。
long count = students.stream().filter(s -> s.getGrade() >= 60) // 中间操作,返回的是流.distinct() // 中间操作,返回的是流.limit(50) // 中间操作,返回的是流.count(); // 终端操作,返回执行结果System.out.println(count);
- Stream接口的distinct方法,用于元素的去重,是一个中间操作。它会返回一个没有重复元素的流,当元素为对象类型时,需要根据流所生成元素的hashCode和equals方法判断是否重复。
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);numbers.stream().filter(i -> i % 2 == 0) // 筛选偶数.distinct() // 排除重复元素.forEach(System.out::println); // 终端操作,打印元素
- Stream接口的limit(n)方法,该方法用于截短流,限定生成流时的元素的个数是多少,也是一个中间操作。如果流是有序的,它会返回前n个元素;如果流是无序的,比如源是一个Set,它的limit结果也是无序的。
Set<Student> students = new HashSet<>();students.add(new Student(10010, "李小军",77));students.add(new Student(10013, "赵平",89));students.add(new Student(10009, "刘小美",85));// 输出结果是自己内部顺序,非元素放入顺序students.stream().limit(5).forEach(System.out::println);
- Stream接口的anyMatch方法,该方法用于至少匹配一个元素,只要有一个元素匹配就返回true,表示匹配成功。anyMatch方法返回一个boolean,因此是一个终端操作。
Set<Student> students = new HashSet<>();students.add(new Student(10010, "李小军",77));students.add(new Student(10013, "赵平",89));students.add(new Student(10009, "刘小美",85));// 判断是否有李小军这个学生boolean b = students.stream().anyMatch(s -> "李小军".equals(s.getName()));System.out.println(b);
- Stream接口的findAny方法,该方法会返回当前流中的任意一个元素。findAny方法的返回结果是Optional<T>类型,它可以帮我们处理结果为null值的情况,能够防止空指针。这个类也是JDK8新增加的容器类,后面也将会学习其使用。
Set<Student> students = new HashSet<>();students.add(new Student(10010, "李小军",77));students.add(new Student(10013, "赵平",89));students.add(new Student(10009, "刘小美",85));// 返回任意一个学生,并打印Optional<Student> student = students.stream().findAny();System.out.println(student.get());