Java8(jdk1.8)是Java的一个主要版本。Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,其间经历过很多小版本。目前它持函数式编程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等。
截至目前 2021.07.25 JDK已经更新到16
由此可查看1.8所有的更新内容,JDK 8 Update Release Notes (oracle.com)
足以看出学习和使用1.8新特性已经成为必然趋势。那么就让我们看看1.8都有哪些值得注意的亮点。
1、数据结构的优化
1.1、对HashMap的优化
1.8之前的HashMap是由数组+链表的形式存储的,通过对key进行hash算发得到了一个值,映射到数组中,如果数组上已存在这个值,那么就会建立一个链表存储,如果链表也存在这个值,就会在链表的首位插入这个新增的元素。
计算hashcode的算法是,两次异或,
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
首先这种数据结构存在如下问题,
- 死链问题,由于倒序插入,transfer()函数会造成死链的形成(具体分析过程见死链的形成 )。
- 数据丢失,(非线程安全)新添加的元素直接放在slot槽中,一个线程的赋值可能会被另一个线程覆盖掉。在当前线程迁移过程中其他线程新增的元素可能落在已经遍历过的哈希槽中,遍历完成之后,table数组指向了newTable,新增元素就会丢失;多个线程同时执行resize,每个线程都会新建newTab,在迁移完成后,赋值给线程共享变量table,从而会覆盖其他线程的操作,在新表中插入操作的对象会丢失。
1.8做出了如下改动,
- 存储结构由以前单纯的数组+链表,变成了数组+链表/红黑树。当冲突节点(也就是链表上的节点)个数达到7个或者7个以上的时候,将会把链表转换为红黑树,然后继续进行存储,所以HashMap中就可能存在有个数组节点后面是链表,有的是红黑树的情况。那么那么1.8之前的时间复杂度为O(N),但是在1.8版本中时间复杂度会是O(logN),性能有了提升
- hash算发精简化,
return h >>> 16;
。由于HashMap的key进行hash如果冲突之后,使用红黑树比链表性能更好,所以作者觉得就算冲突了也还好,所以简化了hash算法,只是把高16位异或下来了而已 - 在 JDK 1.8 中,将高位也参与计算,目的是为了降低 hash 冲突的概率。
1.2、对ConcurrentHashMap的优化
- 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
- 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+synchronized 保证线程安全。
- 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
- 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量大于 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
- 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。
参考这个就ok:https://segmentfault.com/a/1190000039087868
2、新的编码风格
2.1、Lambda表达式
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用Lambda 表达式可以使代码变的更加简洁紧凑。
语法如下,
(parameters) -> expression或(parameters) ->{statements; }
具有如下重要特征,
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
package com.star.besker;
public class LambdaDemo {
public static void main(String [] args) {
//*************************************************
//类型声明
MathOperation addition = (int a,int b) -> a + b;
//非类型声明
MathOperation subtraction = (a, b) -> a - b;
//大括号返回
MathOperation multiplication = (a, b) -> {
return a * b;
};
//*************************************************
//*************************************************
//不用括号
VoidReturnDemo printer1 = msg -> System.out.println(msg);
int c=2;
//用括号
VoidReturnDemo printer2 = msg -> {
//除此之外你可以做任何你想做的
int a=1;
int b = a + c;//我可以使用c
msg += "helloWorld";
System.out.println(msg);
};
c++;//操作后我又不能使用,这是因为我默认c是final的
//*************************************************
//如何使用这些表达式
LambdaDemo lambdaDemo = new LambdaDemo();
lambdaDemo.operate(1, 2, addition);
lambdaDemo.operate(1, 2, subtraction);
lambdaDemo.operate(1, 2, multiplication);
//具有同等效果的匿名内部类,显然更加繁琐
VoidReturnDemo x = new VoidReturnDemo() {
@Override
public void print(String msg) {
System.out.println("我就是想说内部匿名类和Lambda一样" + msg);
}
};
x.print("helloWorld");
printer1.print("helloLambda");
printer2.print("helloLambda");
}
//具有返回值的接口
interface MathOperation {
int operation(int a, int b);
}
//void接口
interface VoidReturnDemo {
void print(String msg);
}
//使用表达式
private int operate(int a, int b, MathOperation mathOperation) {
return mathOperation.operation(a, b);
}
}
lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在lambda 内部修改定义在域外的局部变量,否则会编译错误。
lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有final 的语义)。
2.2、方法引用
1、方法引用通过方法的名字来指向一个方法。
2、方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
3、方法引用使用一对冒号 :: 。
package com.star.besker;
import java.util.Arrays;
import java.util.List;
public class MethodRefDemo {
//Supplier是1.8的接口,配合lambda使用
@FunctionalInterface
public interface Supplier{
T get();
}
public static MethodRefDemo create(final Supplier supplier) {
return supplier.get();
}
public static void collide(final MethodRefDemo methodRefDemo) {
System.out.println("do noting");
}
public void follow(final MethodRefDemo methodRefDemo) {
System.out.println("do noting");
}
public void repair() {
System.out.println("do noting");
}
public static void main(String [] args) {
//***************************构造器引用***************************
MethodRefDemo refDemo1 = MethodRefDemo.create(MethodRefDemo::new);
MethodRefDemo refDemo2 = MethodRefDemo.create(MethodRefDemo::new);
MethodRefDemo refDemo3 = MethodRefDemo.create(MethodRefDemo::new);
MethodRefDemo refDemo4 = new MethodRefDemo();
List methods = Arrays.asList(refDemo1,refDemo2,refDemo3,refDemo4);
//***************************静态方法引用***************************
methods.forEach(MethodRefDemo :: collide);
//***************************特定类的任意对象方法引用***************************
methods.forEach(MethodRefDemo :: repair);
//***************************特定对象的方法引用***************************
final MethodRefDemo iAmFinal = MethodRefDemo.create(MethodRefDemo::new);
methods.forEach(iAmFinal :: follow);
}
}
2.3、函数式接口
1、函数式接口(FunctionalInterface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
2、函数式接口可以被隐式转换为lambda表达式。
3、函数式接口可以现有的函数友好地支持 lambda。
1.8之前就有函数式接口
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
1.8新增了很多函数式接口,
位于java.util.function包下
package com.star.besker;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class FunctionDemo {
public static void main(String [] args) {
List list = Arrays.asList(1,2,3,4,5,6,7,8);
//传递参数n
calculate(list,n-> true);
//传递参数n运算结果
calculate(list,n-> n%2 == 0);
//传递参数n判断
calculate(list,n -> n > 3);
}
//Predicate 接口是一个函数式接口,它接受一个输入参数 T,返回一个布尔值结果。
public static void calculate(List list, Predicate predicate) {
list.forEach(n -> {
if(predicate.test(n)) {
System.out.println("hello" + n);
}
});
}
}
2.4、接口的默认方法
1、Java 8 新增了接口的默认方法。
2、简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。
3、我们只需在方法名前面加个default关键字即可实现默认方法。
这个比较好理解,但是加入他的目的需要解释下,首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。
public interface defaultMethodInterface {
default void doSometing() {
System.out.println("helloWorld");
}
// 甚至静态方法
static void helloStatic() {
System.out.println("helloStatic");
}
}
3、新的日期API
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
1.8之前的日期类存在出多问题
- 非线程安全---java.util.Date式=是非线程安全的,所有的日期类都是可变的,这是java日期类最大的问题之一。
- 设计很差---java日期/时间类的定义并不一致,在java.util和sql包都含有日期类,此外用于格式化的工具又在java.text包下。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
- 没有有国际化,使用不便 ---时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
java8在java.time包下提供了很多新的API,较为重要的两个是:
- Local - 简化日期时间的处理,没有时区问题
- Zoned - 通过制定的时区处理日期时间。
此外,新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。
package com.star.besker;
import java.time.*;
import static java.time.temporal.TemporalAdjusters.firstDayOfYear;
public class TimeDemo {
public static void main(String [] args) {
//获取当前时间
LocalDateTime currentTime = LocalDateTime.now();
//时间转日期
LocalDate date1 = currentTime.toLocalDate();
//获取具体时间
Month month = currentTime.getMonth();
int day = currentTime.getDayOfMonth();
int seconds = currentTime.getSecond();
//指定具体时间
LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
LocalDate date3 = LocalDate.of(2014, Month.DECEMBER,12);
LocalTime date4 = LocalTime.of(22,15);
//多么简单的字符串解析
LocalTime date5 = LocalTime.parse("20:15:30");
//带有时区的时间
ZonedDateTime zdate1 = ZonedDateTime.parse("2015-12-03T10:15:30+05:30[Asia/Shanghai]");
//获得时区
ZoneId id = ZoneId.of("Europe/Paris");
//默认时区
ZoneId currentZone = ZoneId.systemDefault();
//时间段
LocalDateTime from = LocalDateTime.of(2017, Month.JANUARY, 1, 00, 0, 0); // 2017-01-01 00:00:00
LocalDateTime to = LocalDateTime.of(2019, Month.SEPTEMBER, 12, 14, 28, 0); // 2019-09-15 14:28:00
Duration duration = Duration.between(from, to);
//当然还有各种时间计算
LocalDate localDate = LocalDate.now();
LocalDate localDate1 = localDate.with(firstDayOfYear());
}
}
此外final class TemporalAdjusters 提供了很多静态方法非常方便时间计算,
4、新的判空容器
1、Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
2、Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
3、Optional 类的引入很好的解决空指针异常。
提供了如下方法,
以往大量的判空if让程序员头大,一不下心就会NPE,那么来看看Optional优化后的代码多么美丽吧,
package com.star.besker;
import java.util.Optional;
public class OptionalDemo {
public static void main(String [] args) {
Integer v1 = null;
Integer v2 = new Integer(6);
//允许传入null
Optional a = Optional.ofNullable(v1);
//如果传入null会抛出NPE
Optional b = Optional.of(v2);
sum(a, b);
}
public static Integer sum(Optional a, Optional b) throws Exception {
System.out.println("我们可以提前判断包装后的参数是否为空" + a.isPresent());
//我们也可以防止a为null,但在某些业务场景下我们希望代码继续
Integer v1 = a.orElse(new Integer(0));
//或者抛出异常
Integer v3 = a.orElseThrow(Exception::new);
//get方法可以获取参数
Integer v2 = b.get();
return v1 + v2;
}
}
5、新的JavaScript引擎
从JDK8开始,Nashorn引擎开始取代Rhino (jdk6、7中)成为java的嵌入式js引擎,它将js代码编译为java字节码,与先前的Rhino的实现相比,性能提升了2到10倍。
jjs是java8中一个新的命令行工具,jjs能够在控制台执行java中js脚本代码。
例如,编写一个sample.js 内容如下
print("hello world")
保存文件,然后在当前目录打开控制台窗口,执行
jjs sample.js
即可在看到结果 ——在控制台输出了 hello world 字符串。
Java中调用js示例,
package com.star.besker;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class CallJSByJavaDemo {
public static void main(String[] args) {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("Nashorn");
String js = "var a=10; var b=10; var c = a+b;";
Double x = null;
try {
x = (Double) engine.eval(js);
} catch (ScriptException e) {
e.printStackTrace();
}
System.out.println(x);
}
}
6、标准化Base64
在Java8中,Base64编码已经成为Java类库的标准。
Base64工具类提供了一套静态方法获取下面三种BASE64编解码器:
- 基本:输出被映射到一组字符A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持A-Za-z0-9+/。
- URL:输出映射到一组字符A-Za-z0-9+_,输出是URL和文件。
- MIME:输出隐射到MIME友好格式。输出每行不超过76字符,并且使用'\r'并跟随'\n'作为分割。编码输出最后没有行分割。
package com.star.besker;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.util.UUID;
public class Base64Demo {
public static void main(String args[]) throws UnsupportedEncodingException {
// 使用基本编码
String base64encodedString = Base64.getEncoder().encodeToString("runoob?java8".getBytes("utf-8"));
System.out.println("Base64 编码字符串 (基本) :" + base64encodedString);
// 解码
byte[] base64decodedBytes = Base64.getDecoder().decode(base64encodedString);
System.out.println("原始字符串: " +new String(base64decodedBytes,"utf-8"));
base64encodedString = Base64.getUrlEncoder().encodeToString("TutorialsPoint?java8".getBytes("utf-8"));
System.out.println("Base64 编码字符串 (URL) :" + base64encodedString);
StringBuilder stringBuilder =new StringBuilder();
for (int i =0; i <10; ++i) {
stringBuilder.append(UUID.randomUUID().toString());
}
byte[] mimeBytes = stringBuilder.toString().getBytes("utf-8");
String mimeEncodedString = Base64.getMimeEncoder().encodeToString(mimeBytes);
System.out.println("Base64 编码字符串 (MIME) :" + mimeEncodedString);
}
}
7、新的数据集合处理API
Stream的出现可以有效地减少对于几何元素的遍历,聚合,计数等等操作所带来的大量重复且啰嗦的代码,但Stream 的本质仍然是语法糖,不过经过Stream优化的代码更加优美,简洁。
简单的Stream使用方式,
package com.star.besker;
import com.star.besker.entities.Payment;
import java.util.ArrayList;
import java.util.List;
public class StreamDemo {
public static void main(String[] args) {
List demoList = mockList(3);
//原始的遍历
for (Payment payment : demoList) {
if(payment.getId() > 2) {
System.out.println(payment.getSerial());
}
}
//Lambda表达式的遍历
demoList.forEach(payment -> {
if (payment.getId() > 1) {
System.out.println(payment.getSerial());
}
});
//Stream的遍历
demoList.stream().filter(payment -> payment.getId() > 2).forEach(payment -> System.out.println(payment.getSerial()));
}
private static List mockList(int length) {
List demoList = new ArrayList<>();
for (int i=0; i > length; i++) {
Payment payment = new Payment();
payment.setId(Long.valueOf(i));
payment.setSerial(i+"hello");
demoList.add(payment);
}
return demoList;
}
}
Stream的工作原理
java8的流式处理极大的简化了对于集合的操作,实际上不光是集合,包括数组、文件等,只要是可以转换成流,我们都可以借助流式处理,类似于我们写SQL语句一样对其进行操作。java8通过内部迭代来实现对流的处理,一个流式处理可以分为三个部分:转换成流、中间操作、终端操作。
以集合为例,一个流式处理的操作我们首先需要调用stream()函数将其转换成流,然后再调用相应的中间操作达到我们需要对集合进行的操作,比如筛选、转换等,最后通过终端操作对前面的结果进行封装,返回我们需要的形式。
直接上代码看看这些操作
package com.star.besker;
import com.star.besker.entities.Payment;
import java.util.*;
import java.util.stream.Collectors;
public class StreamDemo {
public static void main(String[] args) {
List demoList = mockList(3);
//集合如何获取Stream
demoList.stream();
//map获取Stream
new HashMap().entrySet().stream();
//数组获取Stream
Arrays.asList(1,2).stream();
//filter
demoList.stream().filter(payment -> payment.getId() < 3).collect(Collectors.toList());
//distinct
demoList.stream().distinct().collect(Collectors.toList());
//limit
demoList.stream().limit(3).collect(Collectors.toList());
//skip
demoList.stream().skip(2).collect(Collectors.toList());
//映射,主要包含两类映射操作:map和flatMap
//map 我们可以映射某些属性到新的集合
demoList.stream().map(Payment::getSerial).collect(Collectors.toList());
//或者直接映射为另一种类型然后求和
demoList.stream().mapToDouble(Payment::getId).sum();
//flatMap,flatMap与map的区别在于 flatMap是将一个流中的每个值都转成一个个流,
//终端操作
//allMatch
demoList.stream().allMatch(payment -> payment.getSerial().endsWith("hello"));
//anyMatch
demoList.stream().anyMatch(payment -> payment.getSerial().endsWith("hello"));
//noneMatch
demoList.stream().noneMatch(payment -> payment.getSerial().endsWith("hello"));
//findFirst
demoList.stream().findFirst();
//finAny
demoList.stream().allMatch(payment -> payment.getSerial().endsWith("hello"));
//归约操作
demoList.stream().map(Payment::getId).reduce(Long :: sum);
//收集操作
long count1 = demoList.stream().collect(Collectors.counting());
long count2 = demoList.stream().count();
//求最大最小值
demoList.stream().collect(Collectors.maxBy((s1, s2) -> (int) (s1.getId() - s2.getId())));
demoList.stream().collect(Collectors.maxBy(Comparator.comparing(Payment::getId)));
demoList.stream().collect(Collectors.minBy(Comparator.comparing(Payment::getId)));
//求和
demoList.stream().collect(Collectors.summarizingLong(Payment::getId));
//平均值
demoList.stream().collect(Collectors.averagingLong(Payment::getId));
//字符拼接
demoList.stream().map(Payment::getSerial).collect(Collectors.joining(","));
//分组
demoList.stream().collect(Collectors.groupingBy(Payment::getId));
//分区
demoList.stream().collect(Collectors.partitioningBy(payment -> payment.getId() > 2));
}
private static List mockList(int length) {
List demoList = new ArrayList<>();
for (int i=0; i > length; i++) {
Payment payment = new Payment();
payment.setId(Long.valueOf(i));
payment.setSerial(i+"hello");
demoList.add(payment);
}
return demoList;
}
}
并行处理流
流式处理中的很多都适合采用 分而治之 的思想,从而在处理集合较大时,极大的提高代码的性能,java8的设计者也看到了这一点,所以提供了 并行流式处理。上面的例子中我们都是调用stream()方法来启动流式处理,java8还提供了parallelStream()来启动并行流式处理,parallelStream()本质上基于java7的Fork-Join框架实现,其默认的线程数为宿主机的内核数。
启动并行流式处理虽然简单,只需要将stream()替换成parallelStream()即可,但既然是并行,就会涉及到多线程安全问题,所以在启用之前要先确认并行是否值得(并行的效率不一定高于顺序执行),另外就是要保证线程安全。此两项无法保证,那么并行毫无意义,毕竟结果比速度更加重要,以后有时间再来详细分析一下并行流式数据处理的具体实现和最佳实践。
//parallelStream是利用多线程进行的,这可以很大程度简化我们使用并发操作。
demoList.parallelStream().forEach(payment -> System.out.printf(payment.getSerial()));
8、JVM内存模型的优化
在JDK1.7及以前,HotSpot虚拟机将java类信息、常量池、静态变量、即时编译器编译后的代码等数据,存储在Perm(永久带)里(对于其他虚拟机如BEA JRockit、IBM J9等是不存在永久带概念的),类的元数据和静态变量在类加载的时候被分配到Perm里,当常量池回收或者类被卸载的时候,垃圾收集器会回收这一部分内存,但效果不太理想。
JDK1.8时,HotSpot虚拟机对JVM模型进行了改造,将类元数据放到了本地内存中,将常量池和静态变量放到了Java堆里,HotSpot VM将会为类的元数据明确的分配与释放本地内存,在这种架构下,类元数据就突破了-XX:MaxPermSize的限制,所以此配置已经失效,现在可以使用更多的本地内存。这样一定程度上解决了原来在运行时生成大量的类,从而经常Full GC的问题——如运行时使用反射、代理等。
移除的原因有两点,
- 由于 永久代(PermGen)内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM错误。
- 移除 永久代(PermGen)可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。