文章目录
JDK8新特性
1.Lambda表达式
1.1概述
-
lambda表达式是匿名内部类的一种简写形式,减少代码量,更加的简洁。
-
lambda表达式只能对函数式接口进行简化
-
函数式接口即是只有一个抽象方法的接口,可以通过加上@FunctionalInterface注解,来判断接口是否是函数式接口
1.2使用
语法规则: lambda表达式的专有符号 "->"也被称之为箭头符号,左边的是参数列表,右边是表达式的逻辑代码。
首先我们先准备一个函数式接口,@FunctionInterface注解只是在编译阶段检查该接口是否是函数接口。
@FunctionalInterface
public interface MyFunction{
void print();
}
假设我们不使用lambda表达式
public class TestFunction {
public static void main(String[] args) {
MyFunction myFunction = new MyFunction() {
@Override
public void print() {
System.out.println("我使用匿名内部类的形式创建接口的实现");
}
};
myFunction.print();
}
}
替换成lambda表达式后
public class TestFunction {
public static void main(String[] args) {
MyFunction myFunction = () -> {
System.out.println("通过使用Lambda表达式的形式进行打印");
};
myFunction.print();
}
}
这里我们做出了什么修改呢?
- 外面的类体已经不需要了
-> 左边的括号是print方法的参数列表,-右边是print方法的方法体
细节:
- 如果没有参数,则必须加个() 占位
- 如果只有一个参数,则需要加参数名,可以不加()
- 如果有两个以上的参数,必须加()
这里我们再简化一下
public class TestFunction {
public static void main(String[] args) {
MyFunction myFunction = () -> System.out.println("通过使用Lambda表达式的形式进行打印");
myFunction.print();
}
}
这里我们去掉了方法的{},注意:只有在方法体中的代码只有一行的时候,才可以去掉{},否则必须加上。
在实际的开发中,可以怎么使用函数式接口呢,通常我们会将其作为方法的参数进行传递。
举一个案例,比如一个数组的遍历,我们只想关注如何处理每一条数据,而不是循环的操作。
首先定义一个可以接收数据的接口,jdk8已经提供了一个带参无返回结果的函数式接口Consumer,这里直接使用他
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
}
定义一个方法,用于遍历数组,但是将对数据的操作转交给方法的使用者.
public class TestFunction {
public static void main(String[] args) {
String[] nameList = new String[]{"张三","李四","王五","赵六"};
forEach(nameList,name -> System.out.println(name + "正在被打印"));
}
//这个方法用于遍历传递进来的数组
public static void forEach(String[] value, Consumer<String> consumer){
for (String s : value) {
//把每一条数据的处理权交给了接口的实现,即在调用处使用lambda表达式简化的实现代码
consumer.accept(s);
}
}
}
像集合的forEach方法就类似于这种,感兴趣可以看看集合中forEach方法的源码。
public void forEach(Consumer<? super E> action) {
//只展示关键代码
for (int i = 0; modCount == expectedModCount && i < size; i++)
//elementAt方法用于根据i索引,在es数组中获取指定的元素,传递给函数式接口进行处理
action.accept(elementAt(es, i));
}
public static void main(){
new ArrayList().forEach(ele -> System.out.println(ele)); //此处即使用集合的forEach方法打印数据。
}
像Java中还内置了许多的函数式接口,可以J根据实际开发需求直接使用。
- Consumer : 单个参数没有返回值
- Supplier : 无参有返回值
- Function: 单个参数带有返回值
- Predicate: 单个参数,返回值为boolean
2.方法引用
2.1概述
在实现函数式接口方法的时候,如果函数式接口需要实现的方法功能,在其他方法中已经实现了,想要进行复用,只要函数式接口方法的声明和需要复用的方法完全一致,则可以直接引用。
2.2 使用
方法引用的关键字为 ::
语法格式:
- 如果引用的是实例方法: 对象名::方法名
- 如果引用的是静态方法: 类名::方法名
举例使用方法引用,简化上面说到的集合中forEach打印数据的lambda表达式
//使用lambda表达式简化打印
new ArrayList<>().forEach(ele->System.out.println(ele));
//可以看到ArrayList集合当中forEach方法的函数式接口是Consumer
public void forEach(Consumer<? super E> action) {}
public interface Consumer<T> {
//consumer当中提供的方法是有单个参数无返回值的。
void accept(T t);
}
//我们这里的需求是打印集合中的数据
//则需要去引用别的方法中,和函数式接口的参数、返回值定义一致的方法,而jdk当中的println方法,刚好与其一致
public void println(Object x) {}
//同样也是单个参数没有返回值,刚好我们的需求是打印数据,可以直接套用
//使用方法引用继续简化lambda表达式
new ArrayList().forEach(System.out::println)
//解释一下, out是System类中的一个属性,是一个PrintStream类,而println是PrintStream类中的方法
// 所以先通过System.out获取到PrintStream类的对象 然后out::println 来进行方法引用,对应的是 对象名::方法名
为了更加清晰地理解
我们自己定义一个打印类,编写一个静态方法 ,静态方法的参数、返回值和lambda简化的函数式接口的方法定义一致。
public class MyPrint {
//单个参数,无返回值,和Consumer接口保持一致
public static void write(Object obj){
System.out.println(obj);
}
}
进行引用
new ArrayList<>().forEach(MyPrint::write); //使用类名::方法名进行 方法引用
2.3注意
有一种特殊情况,类名::实例方法,这种在Optional工具类和Mybatis-plus的代码里面多次使用,这种方式只能在特定的情况下使用。
规则:
- 函数式接口的参数必须比引用的方法多一个
- 从函数式接口的方法的参数第二个和引用方法的第一个参数开始对应,必须参数列表一致,返回值也得一致。
- 函数式接口的方法第一个参数是实例方法所在的对象。
测试
//定义一个接口,首先接口得满足第一个规则,参数比引用的方法多一个,所以至少得有一个参数
interface MyFunction{
void test(? args);
}
//再满足第二个规则,接口方法的第二个参数和引用方法的第一个参数开始依次对应,我等会引用的是无参方法,所以接口方法中定义一个参数就够了。
//现在参数的类型怎么确定呢?
//满足第三个规则,接口方法的第一个参数是实例方法所在的对象。如我们要引用测试类的实例方法,那么就要把测试类的类名写上去
interface MyFunction{
void test(Test test);
}
//现在编写一个测试类
public class Test{
public static void main(String[] args){
MyFunction myFunction = Test::myMethod;
//这里调用test方法就会走myMethod方法里的逻辑
myFunction.test(new Test());
}
//因为函数式接口定义的方法只有一个参数,所以对应引用的只能是无参方法。
public void myMethod(){
System.out.println("Hello");
}
}
现在的函数式接口是写死的Test,那就只能引用Test里面符合规则的实例方法
我们利用泛型改造一下更具有通用性。
interface MyFunction<T>{
void test(T t);
}
//现在编写一个测试类
public class Test{
public static void main(String[] args){
MyFunction<Test> myFunction = Test::myMethod;
//这里调用test方法就会走myMethod方法里的逻辑
myFunction.test(new Test());
}
//因为函数式接口定义的方法只有一个参数,所以对应引用的只能是无参方法。
public void myMethod(){
System.out.println("Hello");
}
}
3.Stream API
3.1概述
提供了一系列对集合、数组、IO等类中数据以流的形式来进行处理的方法,提高程序的开发效率,使代码更加简洁优雅。
3.2使用
3.2.1常用的方法
(1)filter(Predicate<? super T> predicate) 根据函数式接口提供的过滤规则对流中的元素进行过滤。
(2)concat(Stream<? extends T> a, Stream<? extends T> b) 将两个流对象拼接成一个流对象。Stream的静态方法
(3)count() 统计流对象中元素的个数,返回long类型的个数
(4)distinct() 对流中数据进行去重
(5)forEach(Consumer<? super T> action) 对集合中元素进行遍历
(6)limit(long maxSize) 获取限定个数的流对象
(7)skip(long n) ;跳过n 个元素后获取剩余的元素
(8)sorted() 对流中元素进行排序。
注意:
- 其中 filter、distinct、limit、skip、sorted 方法的返回值都是流对象,所以可以采用链式编程的形式。
- count、forEach都是用于对最终结果的处理,会终止流的操作。
3.2.2 Stream流的常用创建方式
1.从集合创建流
List<String> list = Arrays.asList("a", "b", "c");
//调用stream()方法
Stream<String> stream = list.stream();
2.从数据创建流
String[] arr = {"a","b","c"};
Stream<String> stream = Arrays.stream(arr);
案例:只保留数组中大于5的元素,并进行打印
int[] ints = {1, 2, 3, 4, 5, 6, 4, 7, 34, 234};
Arrays.stream(ints).filter( num -> num > 5).forEach(System.out::println);
//解释:
/*
1.Arrays.stream(ints) 将数组转换成流对象
2.filter(num -> num > 5) 方法中传入一个lambda表达式,实现了Predicate函数式接口,将数组中每一个元素num和5进行比较,结果为true则会保留在流的结果中
3.forEach() 流的终止操作,打印流中的数据。
*/
在开发中可以用于对数据库查询出来的数据,使用Java中的Stream API进行过滤,返回给前端。
4. Optional 工具类
4.1概述
针对null值判断以及对空值的处理,使代码更加优美
4.2使用
4.2.1 如何创建Optional对象
//要求传入的obj不能是空值,否则会报NullPointerException空指针异常
Optional.of(obj)
//返回一个空的Optional对象
Optional.empty();
//更加智能。支持空值,如果传入有值底层会调用of方法 没有值则调用empty方法
Optional.ofNullable(obj);
4.2.2 Optional类的常用方法
-
get() 获取真实的值,如果为空则会抛出异常。
-
isPresent() 如果存在值,则返回true,反之false
-
isEmpty() 如果不存在值,则返回true,反之false
-
isPresent(Consumer<? super T> action) 如果存在值,则执行传入的接口的方法。
-
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) 如果存在值,调用Consumer接口的方法,不存在则调用Runnable接口的方法
-
filter(Predicate<? super T> predicate) 如果存在值,则对值进行过滤,Predicate接口会返回一个boolean,如果不为true则返回一个空的Optional对象
-
Optional<U> map(Function<? super T, ? extends U> mapper) //如果值为空会抛出异常,如果不为空则会将值进行处理并返回一个结果。
-
public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) //如果值为空反返回一个空的Optional对象,不会空则对值进行处理后,返回一个结果,如果返回的结果为null,则会抛出异常
-
Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {} //如果有值的话,则直接返回值,如果没有值的话,则返回Suppplier接口返回的值,该值为空会抛出异常
-
T orElse(T other) //如果有值则直接返回值,没有值返回other
-
T orElseGet(Supplier<? extends T> supplier) //如果有值则直接返回值,没有值返回Supplier接口返回的值
-
T orElseThrow() //如果值为空,则抛出异常,否则返回value值
-
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) //如果值不为空,则返回值,否则会抛出Supplier返回的异常对象
5.新日期类型
5.1为什么使用新日期类型
传统日期类型的缺陷:
- 设计不合理,使用步骤繁琐,不方便
- 对象是可变的,修改后无法保存刚开始创建时的对象。
- 线程不安全
- 只能精确到毫秒,使用过程中时间可能会出现误差。
新日期类型的优点:
- 设计合理,使用方便
- 对象不可变,每次对对象的操作都会返回一个新的对象
- 线程安全
- 能够精确到纳秒
5.2常用的新日期类型(分类)
代替Calendar:
- LocalTime: 时分秒
- LocalDate: 年月日
- LocalDateTime: 年月日时分秒
- ZoneId: 时区
- ZonedDateTime: 带时区的时间
代替Date:
- Instant: 时间戳/时间线
代替SimpleDateFormat:
- DateTimeFormatter: 用于时间的格式化/解析
其他补充:
- Period: 计算两个日期差
- Duration: 计算两个时间差
5.3使用
public class CalendarReplaceTest {
public static void main(String[] args) {
LocalDate now = LocalDate.now();
//获取年
int year = now.getYear();
//获取月
int monthValue = now.getMonthValue();
//获取日
int dayOfMonth = now.getDayOfMonth();
System.out.printf("%s年%s月%s日%n",year,monthValue,dayOfMonth);
//plus开头的方法即在当前时间上指定新增时间,新增后返回的是一个新的对象,旧的对象不会被修改
LocalDate localDate = now.plusYears(2);
System.out.println(localDate);
LocalDate localDate1 = now.plusDays(2);
System.out.println(localDate1);
LocalDate localDate2 = now.plusMonths(2);
System.out.println(localDate2);
//minus开头的方法是减少,其他的同理就不演示了
LocalDate localDate3 = now.minusMonths(2);
System.out.println(localDate3);
//with开头的方法是修改
LocalDate localDate4 = now.withYear(2029);
System.out.println(localDate4);
LocalDate now1 = LocalDate.now();
LocalTime now2 = LocalTime.now();
//将LocalDate和LocalTime合并成一个LocalDateTime对象
LocalDateTime now3 = LocalDateTime.of(now1, now2);
System.out.println(now3);
//LocalDateTime转LocalDate
LocalDate localDate5 = now3.toLocalDate();
System.out.println(localDate5);
//LocalDateTime转LocalTime
LocalTime localTime = now3.toLocalTime();
System.out.println(localTime);
//获取默认的时区,通过时区可以获取不同国家的时间
System.out.println(ZoneId.systemDefault());
//获取Java支持的所有时区
System.out.println(ZoneId.getAvailableZoneIds());
ZoneId zoneId = ZoneId.of("America/New_York");
//通过时区来获取当前时间
System.out.println(ZonedDateTime.now(zoneId));
}
}
像LocalDate、LocalTime、LocalDateTime的使用方式基本一致,只是表示的时间不同,这里就不一一演示了。
Instant
public class InstantTest {
public static void main(String[] args) {
//获取当前时间
Instant now = Instant.now();
System.out.println(now);
//将instant对象转成LocalDateTime对象
LocalDateTime localDateTime = LocalDateTime.ofInstant(now, ZoneId.systemDefault());
System.out.println(localDateTime);
//获取从1970年来的秒数
System.out.println(now.getEpochSecond());
//从时间线开始,获取从第二个开始的纳秒数
System.out.println(now.getNano());
}
}
DateTimeFormatter
public class DateTimeFormatterTest {
public static void main(String[] args) {
//根据指定日期格式创建DateTimeFormatter对象
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
//将LocalDateTime格式化成特定格式的字符串
String format = dtf.format(LocalDateTime.now());
System.out.println(format);
//调用Local开头日期对象的parse方法,可以将字符串按照特定格式进行转换成对象的Local日期对象,前提要符合规则
LocalDate now = LocalDate.parse("2022-11-11", dtf);
System.out.println(now);
}
}
Duration
public class OtherTest {
public static void main(String[] args) {
//Duration 时间差
//开始时间
LocalTime start = LocalTime.now();
//结束时间啊in
LocalTime end = LocalTime.of(start.getHour() + 2, start.getMinute() + 1);
//将差值信息封装在Duration信息中
Duration between = Duration.between(start, end);
//两个日期之间的小时差值
long hours = between.toHours();
//两个日期之间的分钟差值
long minutes = between.toMinutes();
System.out.println(hours);
System.out.println(minutes);
}
}
Period和Duration的使用方式基本一致,只是处理的时间不同。