从零开始学Java8新特性(使用方法和注意事项)
简介
Java8引入了很多新特性,作为一个懒人,一直觉得掌握部分常用的即可,但在新项目中发现同事代码充分利用了Java8特性,不仅代码优雅,而且层次分明,这些特性很大程度改变了我们的代码风格。如果不学习,很有可能看不懂同事的代码,那真的就尴尬啦。故萌生重学一遍Java8新特性,查漏补缺,也供将来面试复习使用。
1. Lambda表达式及函数表达式
1.1 介绍
Lambda表达式是Java8中引入的一种新的语法,用于简化匿名内部类的创建。我们可以把Lambda表达式看成一段可以传递的代码,使用它,可以使代码更加简洁、紧凑。使用Lambda表达式时,接口必须是函数式接口(即只有一个抽象方法的接口)。
1.2 Lambda表达式demo
( )-> System.out.println("hello,world");
(int x, int y) -> x + y;
(x, y) -> {
int sum = x + y;
System.out.println("Sum: " + sum);
return sum;
}
- ()里面是参数列表,Lambda表达式可以包括零或多个参数,用 “,”分隔
- Lambda的主体可以是一个表达式或者一组表达式,它的结果将成为Lambda表达式的返回值。如果是一组表达式,必须使用“{}”包裹。
1.3 Lambda表达式声明
Demo:
<函数式接口> <变量名> = (参数1,参数2...) -> {
//方法体
}
- Lambda表达式的参数类型可以省略。
// 参数类型明确
Function<Integer, Integer> square = (Integer x) -> x * x;
// 参数类型推断
Function<Integer, Integer> squareInferred = (x) -> x * x;
- Lambda表达式的返回值类型可以省略。
// 显式声明返回类型
Comparator<String> lengthComparator = (String s1, String s2) -> s1.length() - s2.length();
// 返回类型推断
Comparator<String> lengthComparatorInferred = (s1, s2) -> s1.length() - s2.length();
- Lambda表达式若访问了局部变量,则局部变量必须是final的,不加会自动加,只要修改该局部变量就会报错。
1.3 函数式接口Demo
@FunctionalInterface
interface MyFunctionalInterface {
void myMethod(); // 抽象方法
// 默认方法
default void defaultMethod() {
System.out.println("Default method");
}
// 静态方法
static void staticMethod() {
System.out.println("Static method");
}
}
- 函数式接口必须是只有单一抽象方法的接口,这个抽象方法的方法体由Lambda表达式来实现。
- 如果想在函数式接口增加其他方法而不影响它是函数式接口,可以使用默认方法和静态方法(后面详写)
- 函数式接口通常用“@FunctionInterface”注解来明确它是函数式,使用它后编译器会自动检查这个接口是否符合函数式接口的条件。
2. Stream API
Stream API引入了一种新的抽象序列类型,可以更轻松地对集合进行操作,支持串行和并行。它提供了一套丰富的操作流水线式处理数据,实现更简洁、高效、并行花的集合操作。
2.1 创建流
可以通过集合、数组、I/O通道等方式创建流
List<String> list = Arrays.asList("apple", "banana", "orange");
Stream<String> streamFromList = list.stream();
int[] array = {1, 2, 3, 4, 5};
IntStream streamFromArray = Arrays.stream(array);
Stream<String> streamOfStrings = Stream.of("apple", "banana", "orange");
2.2 中间操作
中间操作是对流进行转换的操作,它不会自动执行,而是在终端操作被调用时才会执行。
List<String> filteredList = list.stream()
.filter(s -> s.startsWith("a"))//过滤
.map(String::toUpperCase)
.distinct()//去重
.sorted()//排序
.collect(Collectors.toList());
2.2.1 map
map用于将每个元素映射为另一个元素,map接收一个函数作为参数,该函数被应用于流中的每一个元素,并将每个输入元素映射为一个输出元素。映射后的结果形成一个新的流,不会改变原始流中的元素。通常用于进行一些转换、提取、计算操作。
具体语法:
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
其中T是流中元素的类型,R是映射后元素的类型。mapper则是一个函数,接收T类型元素,返回R类型结果。
demo:
words.stream().map(String::toUpperCase) -- 将字符串转大写
students.stream().map(Student::getName) --获取对象列表的属性集合
2.2.2 reduce
reduce 合并流的元素并产生单个值。
reduce方法签名:
Stream.java
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
IntStream.java
int reduce(int identity, IntBinaryOperator op);
LongStream.java
long reduce(int identity, LongBinaryOperator op);
- identity = 默认值或初始值,如果缺少这个参数,则没有默认值或初始值,并返回Optional(下面有介绍)
- BinaryOperator = 函数式接口,取两个值产生新的值。BinaryOperator接口用于执行 Lambda 表达式并返回一个 T 类型返回值。
Stream.of(1,2,3).reduce(0,(a,b) -> a+b); -- 求和
public static int sum(int a,int b) { return a + b;}
Stream.of(1,2,3).reduce(0,Integer::sum);
2.3 终端操作
终端操作会触发流的遍历和执行操作。常见包括 forEach、collect、reduce、count等。
list.stream()
.forEach(System.out::println);
List<String> collectedList = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
Optional<String> concatenated = list.stream()
.reduce((s1, s2) -> s1 + ", " + s2);
2.4 并行流
使用parallelStream将流转换为并行流。
List<String> parallelList = list.parallelStream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
2.5 Optional和Nullable支持
Optional用于存放T类型值或者Null,它提供一些接口来判空防止空指针异常,更优雅、更安全。配合Stream流通过findFirst、findAny、orElse等方法处理可能为空的情况。
-- 1. 创建Optional对象
Optional<String> nonEmptyOptional = Optional.of("Hello, Optional!");
String value = /* 可能为null的值 */;
Optional<String> nullableOptional = Optional.ofNullable(value);
-- 2. 获取Optional的值
Optional<String> nonEmptyOptional = Optional.of("Hello, Optional!");
String value = nonEmptyOptional.get(); -- 如果Optional包含Null值,会抛出*NoSuchElementException*异常
String value = nullableOptional.orElse("Default Value");
-- 类似 String value = StringUtils.isNotEmpty(value) : value : "Default Value"
-- 3. 判断是否包含空值
if (nonEmptyOptional.isPresent()) {
// 执行操作
}
-- 4. 条件执行(isPresent()判断是否包含值)
nonEmptyOptional.ifPresent(val -> System.out.println("Value: " + val));
-- 5.默认值和函数处理
-- orElseGet() 方法允许提供一个Supplier,在Option为空时计算默认值
String result = nullableOptional.orElseGet(() -> "Default Value");
-- map() 方法允许对Optional中的值进行转换
Optional<String> upperCaseOptional = nonEmptyOptional.map(String::toUpperCase);
-- 6.过滤
-- filter 方法允许在 Optional 包含值的情况下应用一个条件
Optional<String> filteredOptional = nonEmptyOptional.filter(s -> s.length() > 5);
-- 7.链式调用
-- 可以通过链式调用组合多个操作,使代码更为流畅和可读:
String result = Optional.ofNullable(nullableValue)
.map(String::toUpperCase)
.orElse("Default Value");
-- 8. 其他用法
Optional<String> firstElement = list.stream().findFirst();
6. 收集器
通过collect方法可以使用各种内置的收集器,将流中的元素汇总到一个集合中,如toList、toSet、toMap等。
Map<Integer, List<String>> groupedByLength = list.stream()
.collect(Collectors.groupingBy(String::length));
Set<String> set = stream.collect(Collectors.toSet());
//将流中的元素按照指定的规则收集到一个Map中。
Map<Integer, String> map = stream.collect(Collectors.toMap(String::length, Function.identity()));
//将流中的元素连接成一个字符串。
String result = stream.collect(Collectors.joining(", "));
//根据某个属性对流中的元素进行分组
Map<Integer, List<String>> groupedByLength = stream.collect(Collectors.groupingBy(String::length));
//根据某个条件对流中的元素进行分区,分区只有两个区域(true和false)
Map<Boolean, List<String>> partitioned = stream.collect(Collectors.partitioningBy(s -> s.length() > 3));
//统计流中的元素个数。
long count = stream.collect(Collectors.counting());
//统计流中的元素的统计信息,包括count、sum、min、average、max
IntSummaryStatistics stats = stream.collect(Collectors.summarizingInt(String::length));
//对流中的元素进行归约操作
Optional<String> concatenated = stream.collect(Collectors.reducing((s1, s2) -> s1 + s2));
//对收集器的结果进行转换
List<String> result = stream.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
3. 函数式接口
4. 默认方法
接口中新增了static和default方法,这两种方法可以拥有方法体。
4.1 static方法
public interface DefalutTest {
static int a =5;
default void defaultMethod(){
System.out.println("DefalutTest defalut 方法");
}
int sub(int a,int b);
static void staticMethod() {
System.out.println("DefalutTest static 方法");
}
}
接口里的静态方法,即static方法不会被继承或者实现(即不能被实现类调用,只能被自身调用),但是静态变量会被继承。接口里s
4.2 default方法
default方法可以被子接口继承,被实现类调用、重写。
如果一个类实现了多个接口,且这些接口没有继承关系,如果这些接口有同名同参数的default方法,则接口实现类会报错,必须指定实现了哪个接口的default方法。
5. 新的日期/时间API
Java8引入了全新的日期和时间API,位于 java.time 包下。这个API解决了java.util.Date和java.util.Calendar存在的一些问题,并提供了更加清晰、易用、线程安全的日期和时间处理方式。
5.1 LocalDate
LocalDate表示一个不带时区信息的日期。它提供了许多方法来处理日期,例如计算两个日期之间的天数差异、获取年、月、日等。
//1. 获取当前日期
LocalDate currentDate = LocalDate.now();
System.out.println("当前日期: " + currentDate);
//结果:当前日期: 2023-12-12
//2. 获取年月日
int year = currentDate.getYear();
int month = currentDate.getMonthValue();
int day = currentDate.getDayOfMonth();
//3. 计算两个日期之间天数差异
LocalDate anotherDate = LocalDate.of(2023, 12, 31);
long daysDiff = currentDate.until(anotherDate, ChronoUnit.DAYS);
System.out.println("两个日期之间的天数差异: " + daysDiff);
//4. 检查是否为闰年
boolean isLeapYear = currentDate.isLeapYear();
System.out.println("是否为闰年: " + isLeapYear);
//5. 增加或减少天数、月数、年数
LocalDate newDate = currentDate.plusDays(7);
System.out.println("增加7天后的日期: " + newDate);
LocalDate minusMonth = currentDate.minusMonths(2);
System.out.println("减少2个月后的日期: " + minusMonth);
//6. 比较两个日期
LocalDate earlierDate = LocalDate.of(2023, 6, 1);
LocalDate laterDate = LocalDate.of(2023, 9, 1);
boolean isBefore = earlierDate.isBefore(laterDate);
boolean isAfter = laterDate.isAfter(earlierDate);
System.out.println("前一个日期是否在后一个日期之前: " + isBefore);
System.out.println("后一个日期是否在前一个日期之后: " + isAfter);
5.2 LocalTime
LocalTime表示一个不带时区信息的时间。它提供了获取小时、分钟、秒等信息的方法。
//1. 获取当前时间
LocalTime currentTime = LocalTime.now();
System.out.println("当前时间: " + currentTime);
//2. 获取小时、分钟、秒
int hour = currentTime.getHour();
int minute = currentTime.getMinute();
int second = currentTime.getSecond();
System.out.println("小时: " + hour);
System.out.println("分钟: " + minute);
System.out.println("秒: " + second);
//3. 获取纳秒(nanoseconds)
int nano = currentTime.getNano();
System.out.println("纳秒: " + nano);
//4. 构造新的时间对象
LocalTime specificTime = LocalTime.of(12, 30, 45);
System.out.println("指定时间: " + specificTime);
//5. 增加或减少小时、分钟、秒
LocalTime newTime = currentTime.plusHours(2);
System.out.println("增加2小时后的时间: " + newTime);
LocalTime minusMinutes = currentTime.minusMinutes(15);
System.out.println("减少15分钟后的时间: " + minusMinutes);
//6. 判断时间先后
LocalTime earlierTime = LocalTime.of(10, 30);
LocalTime laterTime = LocalTime.of(15, 45);
boolean isBefore = earlierTime.isBefore(laterTime);
boolean isAfter = laterTime.isAfter(earlierTime);
System.out.println("前一个时间是否在后一个时间之前: " + isBefore);
System.out.println("后一个时间是否在前一个时间之后: " + isAfter);
//7. 格式化时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
String formattedTime = currentTime.format(formatter);
System.out.println("格式化后的时间: " + formattedTime);
5.3 LocalDateTime
LocalDateTime表示一个不带时区信息的日期和时间。它是LocalDate和LocalTime的组合。
//1. 获取当前日期和时间:
LocalDateTime currentDateTime = LocalDateTime.now();
System.out.println("当前日期和时间: " + currentDateTime);
//当前日期和时间: 2023-12-12T17:00:15.636
//2. 获取日期和时间的各个部分:
int year = currentDateTime.getYear();
int month = currentDateTime.getMonthValue();
int day = currentDateTime.getDayOfMonth();
int hour = currentDateTime.getHour();
int minute = currentDateTime.getMinute();
int second = currentDateTime.getSecond();
System.out.println("年: " + year);
System.out.println("月: " + month);
System.out.println("日: " + day);
System.out.println("小时: " + hour);
System.out.println("分钟: " + minute);
System.out.println("秒: " + second);
//3. 构造新的日期和时间对象:
LocalDateTime specificDateTime = LocalDateTime.of(2023, 6, 15, 12, 30);
System.out.println("指定的日期和时间: " + specificDateTime);
//4. 增加或减少年、月、日、小时、分钟、秒:
LocalDateTime newDateTime = currentDateTime.plusYears(1)
.minusMonths(2)
.plusDays(7)
.minusHours(3)
.plusMinutes(15)
.minusSeconds(30);
System.out.println("修改后的日期和时间: " + newDateTime);
//5. 判断日期和时间先后:
LocalDateTime earlierDateTime = LocalDateTime.of(2023, 6, 1, 10, 30);
LocalDateTime laterDateTime = LocalDateTime.of(2023, 6, 1, 15, 45);
boolean isBefore = earlierDateTime.isBefore(laterDateTime);
boolean isAfter = laterDateTime.isAfter(earlierDateTime);
System.out.println("前一个日期和时间是否在后一个日期和时间之前: " + isBefore);
System.out.println("后一个日期和时间是否在前一个日期和时间之后: " + isAfter);
//6. 格式化日期和时间:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = currentDateTime.format(formatter);
System.out.println("格式化后的日期和时间: " + formattedDateTime);
5.4 Instant
Instant表示时间线上的一个点,通常与协调世界时(UTC)一起使用。它可以用于表示时间戳。
Instant instant = Instant.now();
5.5 Duration和Period
Duration表示两个时间点之间的持续时间,而Period表示两个日期之间的间隔。
LocalDateTime start = LocalDateTime.of(2023, 1, 1, 12, 0);
LocalDateTime end = LocalDateTime.of(2023, 12, 31, 23, 59);
Duration duration = Duration.between(start, end);
Period period = Period.between(LocalDate.of(2023, 1, 1), LocalDate.of(2023, 12, 31));
5.6 ZoneId和ZonedDateTime
ZoneId表示时区,而ZonedDateTime表示带有时区信息的日期和时间。
ZoneId newYorkZone = ZoneId.of("America/New_York");
ZonedDateTime newYorkTime = ZonedDateTime.now(newYorkZone);
5.7 DateTimeFormatter
DateTimeFormatter用于格式化和解析日期时间对象。
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
5.8 扩展:时间对象的格式化和序列化
由于数据库时区问题或者前后端传参等情况,传时间参数时总会出现各种各样的问题,但如果合理使用@DateTimeFormat 和 @JsonFormat 这两个注解,会省去很多麻烦。
@JsonFormat
作用范围:主要用于Jackson库中,作为字段、方法、构造函数的注解,用于指定在将Java对象序列化成JSON字符串或从JSON字符串反序列化为Java对象时的日期和时间格式。
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat
作用范围:主要用于SpringMVC中,作为Controller方法参数的注解,用于指定日期和时间的格式化方式,将请求中时间字符串参数转化成Date 或 LocalDate 类型。
@RequestMapping("/example")
public String example(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {
// 方法体
}
或者也可以用作请求对象的字段上,用于格式化前端传过来的时间对象。
@JSONField
作用范围:主要用在java反序列化时进行属性的匹配。例如数据里key是name,实体类是userName,则使用@JSONField(name=”name”)则可以实现映射关系。
@JsonProperty
作用范围:主要用在java序列化时,使用@JsonProperty注解里value指定的值,反序列化时会将值赋值到属性字段上面