Java 8(LTS)是Java历史上一个重大的版本更新,发布于2014年3月18日,有Lambad表达式、Stream、Optional、新的日期API等十种新特性。
ps:重要的特性会尽量细讲,不重要的简单概括。
一、Lambda表达式
Lambad表达式被誉为Java 8的最大的特性,标志着Java向函数式边冲迈出了重要的第一步。
1.1 语法
(parameters)->expression
//或者
(parameters)->{statements:}
parameters:参数列表,可以为空或多个参数。
->:操作符,作用是将参数和Lambda主体分开。
expression:返回值,或者在主体中执行的单一表达式。
{statements}:主体部分,如果有多个操作,就需要这种形式。
看一个简单的Lambda表达式,遍历一个list:
public static void main(String[] args) {
List<String> strList = ImmutableList.of("hello", "world", "!!!");
strList.forEach(s-> System.out.println(s));
// strList.forEach(s->{
// if("hello".equals(s)){
// System.out.println(s);
// }
// });
}
1.2 实现原理
网上有人说他不是语法糖,但是这个说法并不准确。Lambda表达式不是匿名内部类的语法糖,但它也是一个语法糖,实现的方式主要是依赖了JVM底层提供的Lambda相关api。
我们对上述例子编译一下:
可以看到只有一个文件,如果是内部类的话编译后应该会有两个class文件才对,所以他不是内部类的语法糖。
下面我们反编译看下:
public class LambdaTest {
public static void main(String[] args) {
ImmutableList strList = ImmutableList.of((Object)"hello", (Object)"world", (Object)"!!!");
strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
}
private static /* synthetic */ void lambda$main$0(String s) {
System.out.println(s);
}
}
可以看到,在foreach中,其实调用了LambdaMetafactory的metaFactory方法,这个方法的第五个参数指定了方法的实现(感兴趣的可以去看下源码),这里其实调了一个lambda$main$0方法进行了输出。
这个比较简单,我们换个稍微复杂点的:
public static void main(String[] args) {
List<String> strList = ImmutableList.of("hello", "world", "!!!");
strList.stream().
filter(s->s.contains("hello"))
.collect(Collectors.toList())
.forEach(s-> System.out.println(s));
}
再反编译一下:
public class LambdaTest {
public static void main(String[] args) {
ImmutableList strList = ImmutableList.of((Object)"hello", (Object)"world", (Object)"!!!");
strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList()).forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.String ), (Ljava/lang/String;)V)());
}
private static /* synthetic */ void lambda$main$1(String s) {
System.out.println(s);
}
private static /* synthetic */ boolean lambda$main$0(String s) {
return s.contains("hello");
}
}
可以看到两个Lambda表达式分别调用了lambda$main$1和lambda$main$0方法。
所以Lambda表达式的实现其实是依赖了一些底层的api,在编译阶段,编译器会把lambda表达式进行解糖,转换成调用内部api的方式。
1.3 使用条件
有两个条件:
必须有相应的函数式接口:也是Java8引入的,下面会介绍到。(第四节)
类型推断机制:也是Java 8优化的类型推断机制,允许编译器根据上下文自动推断Lambda表达式的参数类型,下面也会讲。(第十节)
二、Stream流
Java 8新特性两个最为重要的更新:第一个就是上述的Lambda表达式,另一个则是Stream流。
2.1 相关概念
Stream流是一种对Java集合和表达的高阶抽象,类似于用SQL语句从数据库查询数据的直观方式。Stream API可以极大提高Java程序眼你的生产力,让程序员写出高效率、简洁、优雅的代码。
Stream流具备以下几个特点:
无存储:Stream不是一种数据结构,可以理解为它是某种数据源的视图。数据源:数组、Java容器、I/O channel等。
为函数式编程而生:对Stream的任何修改都不会修改背后的数据源,而是会生成一个新的Stream(线程安全)。
惰性执行:Strean的操作不会立即执行,而是真正需要结果的时候才会执行。
对于Stream流的处理,主要分为三个步骤:流的创建、中间操作以及最终操作。
2.2 流的创建
Java 8 Stream流的创建有很多种,这里就介绍最常用的两种。
2.2.1 已有集合创建
Java 8除了增加了很多Stream相关的类之外,还对集合类自身做了增强:
public static void main(String[] args) {
List<String> strList = ImmutableList.of("hello", "world", "!!!");
//方式一:普通流
Stream<String> stream = strList.stream();
//方式二:并行流
Stream<String> stringStream = strList.parallelStream();
}
2.2.2 Straem创建
可以用Stream提供的方法,直接返回一个流:
public static void main(String[] args) {
Stream<String> stream = Stream.of("hello", "world", "!!!");
}
ps:另外还可以用Stream的builder方法、generate方法或iterate方法,这几种都不常用,感兴趣的可以去试一下。
2.3 中间操作
这些操作允许在Stream上进行一系列的数据处理,常见的操作有fileter、map、limit、skip、sorted、distinst等:
public static void main(String[] args) {
//1、filter:过滤出符合条件的元素
List<String> strList = ImmutableList.of("hello", "world", "!!!");
//过滤出集合中包含“o”的元素,输出结果:hello, world
strList.stream().filter(s -> s.contains("o")).forEach(System.out::println);
//2、map:映射每个元素到对应的结果
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9);
//求平方数,输出结果:9,1,16,1,25,81
list.stream().map(i->i*i).forEach(System.out::println);
//3、limit/skip:limit返回Stream的前面n个元素;skip则是跳过前n个元素
//返回前3个元素,输出结果:3,1,4
list.stream().limit(3).forEach(System.out::println);
//跳过前3个元素,输出结果:1,5,9
list.stream().skip(3).forEach(System.out::println);
//4、sorted:对流进行排序
//默认升序:113459
list.stream().sorted().forEach(System.out::println);
//5、distinct:去重
//输出结果:31459
list.stream().distinct().forEach(System.out::print);
}
2.4 最终操作
Stream的中间操作得到的结果还是一个Stream,那么如何转为我们需要的类型呢?比如计算出流中元素个数、转为集合等,这就是最终操作实现的。
最终操作会消耗流,也就是说在最终操作之后就不能再使用流了。
常用的有forEach、count以及collect:
public static void main(String[] args) {
//1、foreach:迭代流中的数据
List<String> list = Arrays.asList("hello", "world", "!!!");
//输出结果:hello、world
list.stream().limit(2).forEach(System.out::println);
//2、count:统计流中元素个数
//输出结果:2
long o = list.stream().filter(s -> s.contains("o")).count();
//3、collect:就是一个归约操作,将流中元素汇总结果
//输出结果:!!!
list = list.stream().skip(2).collect(Collectors.toList());
}
ps:另外还有匹配相关的api,比如allMatch()、anyMatch()、noneMatch()等,查询相关的min()、max()、findAny等等。
三、Optional
NullPointException是最令我们头疼的异常之一,而Java为了处理这种异常也在一直努力着,比如在Java 8引入了Optional类来减少NEP的发生,Java 14引入了Help NEP来帮助我们更好地排查(这个后面再说)。
举几个常用的例子:
public static void main(String[] args) {
Student stu = createStu();
//创建一个Optional对象
Optional<Student> optional = Optional.ofNullable(stu);
//用法一:判断是否为空,true不为空,false为空
if(optional.isPresent()){
//不为null
Student student = optional.get();
}
//用法二:如果为空则初始化一个Student对象并返回
Student student = optional.orElse(new Student("李四", 20));
//用法三:如果student为空,直接抛异常,否则获取Student对象
Student student1 = optional.orElseThrow(() -> new RuntimeException("Student不存在"));
}
public static Student createStu(){
return new Student("张三",18);
}
四、函数式接口
函数式接口是一个只有一个抽象方法的接口(Single Abstract Method)。Java 8引入的函数式接口主要目的是为了支持Lambda表达式,比如CompletableFuture、Stream类中的方法都有这些函数式接口作为参数。
从本质上说Lambda表达式就是一个函数式接口的实例。
Java 8中新增的函数式接口都在java.util.function包中,我们使用最多的也就四个:
ps:Java 8之前也已经有了大量的函数式接口,比如我们最熟悉的Runnable、Callable、Comparator接口,还有很多很多,这里就不列举了。
五、新的日期API
5.1 为什么要增加新设计新的日期API
在Java 8之前,我们一般用java.util.Date、java.util.Calender或java.text.SimpleDateFormat来处理与时间相关的常见,但是这三个类存在一些问题:
线程不安全。
使用起来比较复杂,通常需要大量的样板代码。
时区处理困难。
java.util.Date类是可变的,这意味着我们可以随时修改它,如果一不小心就会导致数据不一致问题。
基于上述原因,Java重新设计了时间日期的API,主要有Instant、LoacalDate、LocalTiem、LoacalDateTime等。
5.2 Instant
Instant表示时间线上的点,不可变。参考的是标准的Java纪元(epoch),即1970-1-1 0:0:0。
public static void main(String[] args) {
Instant start = Instant.now();
//逻辑
Instant end=Instant.now();
//获取时间差(类似System.currentTimeMillis()功能)
System.out.println("耗时:"+Duration.between(start,end).toMillis()+"ms");
}
5.3 LocalDate
表示不包含时间信息的日期,不可变。ps:DateTimeFormatter也是Java 8新增的类,不可变。
public static void main(String[] args) {
//创建当前日期
LocalDate now = LocalDate.now();
//创建指定日期的LocalDate对象
LocalDate of = LocalDate.of(2024, 8, 29);
//使用DateTimeFormatter解析一个LocalDate对象
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate localDate = LocalDate.parse("2024-08-29",dateTimeFormatter);
//日期计算
//加2年:2026-8-29
LocalDate localDate1 = localDate.plusYears(2);
//加2月
LocalDate localDate2 = localDate.plusMonths(2);
//加2天
LocalDate localDate3 = localDate.plusDays(2);
//加2周
LocalDate localDate4 = localDate.plusWeeks(2);
//减2年:2022-8-29
LocalDate localDate5 = localDate.minusYears(2);
System.out.println("localDate5 = " + localDate5);
//...
}
5.4 Period
用于处理时间间隔的类。
public static void main(String[] args) {
LocalDate start = LocalDate.of(1970, 1, 1);
LocalDate end = LocalDate.of(2024,8,29);
Period period = Period.between(start, end);
//相隔54年7个月零28天
System.out.println("相隔"+period.getYears()+"年"+period.getMonths()+"个月零"+period.getDays()+"天");
}
5.5 LocalTime
获取不包含日期的时间,也就是一天中的时间。
public static void main(String[] args) {
//当前时间:21:46:16.462
LocalTime now = LocalTime.now();
//创建指定时间:13:14:52.000000001
LocalTime time = LocalTime.of(13, 14, 52, 1);
//时间计算
LocalTime localTime = time.plusHours(1);
LocalTime localTime1 = time.minusHours(1);
//...分、秒、纳秒,这里就省略了
//获取时间信息:时、分、秒..
int hour = time.getHour();
//修改时间:14:14:52.000000001,时分秒...
LocalTime localTime2 = time.withHour(14);
}
5.6 LocalDateTime
日期和时间,这里LocalDate和LocalTime结合起来就是了,方法什么的也都一致,这里就不举例了。
5.7 ZoneDateTime
带时区的日期和时间,因为地球上不同区域的时区是不一样的,比如现在北京是晚上,别的地方可能是早晨,所以为了更好地支持全球化的时间操作,有了ZoneDateTime类,用法和LocalTime几乎一样。
public static void main(String[] args) {
//获取所有时区(589个时区)
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
//默认时区:上海
ZonedDateTime now = ZonedDateTime.now();
//上海时间:2024-08-29 22:10:44
String format1 = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
//改为纽约时区
ZonedDateTime zonedDateTime = now.withZoneSameInstant(ZoneId.of("America/New_York"));
//纽约时间:2024-08-29 10:10:44
String format = zonedDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
六、异步编程神器:CompletableFuture类
Java 8引入了一种基于Future的编程模型,旨在解决Future的局限性。
本节可以看作者之前的文章:Java并发编程第7讲——CompletableFuture、Future和ForkJoinPool(万字详解)
七、接口默认方法和静态方法
在Java8之前,我们知道接口的变量必须时public static final修饰的,方法必须时public abstract修饰的,这意味着我们在接口中每新增一个方法,对应的实现类就必须重写这个方法,灵活性有点差。
所以Java 8接口引入了默认方法,实现类可以不强制重写默认方法。有点更像抽象类了。
同时Java 8还允许在接口中定义静态方法,可以直接用类名.方法名直接调用。
public interface DefaultInterFace {
default void defaultMeth(){
System.out.println("默认方法");
}
static void staticMethod(){
System.out.println("静态方法");
}
}
//测试
@Test
void test() {
DefaultInterFace defaultInterFace=new DefaultInterFaceImpl();
//调用默认方法
defaultInterFace.defaultMeth();//默认方法
//调用静态方法
DefaultInterFace.staticMethod();//静态方法
}
八、Base64 API
Base 64编码是一种用64个字符表示二进制的方法,注意它并不是一种加密算法,Base 64常用于在不支持二进制数据的系统间传输二进制数据。
在Java 8之前我们通常需要依赖第三方库Apache Common Code来实现Base64的编码和解码。
这个就简单介绍了,直接看个例子吧:
public static void main(String[] args) {
String str="橡皮人";
//编码
String encode = Base64.getEncoder().encodeToString(str.getBytes());
System.out.println("encode = " + encode);
//解码
byte[] decode = Base64.getDecoder().decode(encode);
System.out.println("decode = " + new String(decode));
}
ps:上述例子是最常见的类型,适用于大多数常见,另外还有URL和文件名安全、MIME类型的Base64编码,直接调用其对应方法即可。
九、类型注解
在Java 8之前,注解仅能用于方法、类、字段上,Java 8引入了类型注解,扩展了注解的应用范围,可以用在任何地方,比如方法的返回类型、变量的类型等,提供了更丰富的编译时检查。
public static void main(String[] args) {
//1、泛型
List<@NonNull String> strList=new ArrayList<>();
strList.add(null);
//2、类型转换
Object obj="橡皮人";
String str= (@NonNull String) obj;
//3、局部变量
@NonNull String s=null;
}
//4、方法返回值上、抛出的异常类型上
//@NonNull仅为说明异常类型上也可以加注解
public static @Positive int t() {
//@Positive方法返回一个正数
return -6;
}
//5、接口上
//@NonNull仅为说明接口上也可以加注解
class Anno implements @NonNull DefaultInterFace{
}
那有什么用呢?这样看不直观,看下下面截图:
它会在编译时,如果不符合注解的语义,会给出提示,从而防止发生未知的错误。
十、类型推断优化
类型推断机制也是慢慢演进而来的,比如Java 5引入了泛型、Java 7引入了"<>"操作符简化泛型实例的创建,而Java 8的优化主要十Lambda表达式。
List<String> list = Arrays.asList("hello", "world", "!!!");
List<String> strings = list.stream()
.filter(s -> s.contains("o"))
.map(String::toUpperCase)
.collect(Collectors.toList());
比如上述filter和map的中间操作,就是使用了类型推断机制,每个操作得输出类型自动称为下一个操作得输入类型。
ps:不使用的话,每个操作都得指定类型,比如"s"就得写为"(String s)"。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。