JDK8
新版任你发,我用Java8
身为使用最多的版本,我们还是需要好好了解JDK8的改动
JDK的常用新特性总结:
-
Lambda 表达式:Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)
-
函数式接口
-
方法引用和构造器调用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
-
接口支持静态方法和默认方法
-
Stream API:新添加的Stream API(java.util.stream)把真正的函数式编程风格引入到Java中
-
Date Time API:加强对日期与时间的处理。
-
Optional类:Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常
-
改进的类型推导
-
JVM方法区:用元空间替代“永久代”
-
Map集合数据结构优化:如HashMap结构由Hash表 -》Hash表+红黑树
还有很多如IO/NIO,Base64等改动,具体看官网
Oracle:What’s New in JDK 8(有中文版)
JVM方法区
永久代
很多人愿意把方法区称为“永久代”,因为HotSpot设计团队把GC分代收集扩展到了方法区,即用“永久代”实现方法区,HotSpot的垃圾收集器可以像管理堆一样管理方法区
然而,看起来使用永久代实现方法区并不是个好办法,-XX:MaxPermSize限制了永久代的大小,更容易遇到内存溢出的问题
- 在JDK7,将放在永久代的字符串常量池、静态变量移到了堆
- 在JDK8,使用元空间(MetaSpace)取代了永久代,元空间使用本地内存而不是虚拟机,即废弃了大小限制-XX:MaxPermSize
Native memory:本地内存,也称为C-Heap,是供JVM自身进程使用的。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC
为什么要移除永久代?
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、永久代大小不容易确定,PermSize指定太小容易造成永久代OOM
3、永久代会为GC带来不必要的复杂度,并且回收效率偏低。
4、Oracle将HotSpot与JRockit合二为一,而JRockit没有“永久代”
Lambda表达式
在之前就写个关于Lambda的文章:Java基础 - 内部类、Lambda表达式
从内部类 -》匿名内部类,JDK8推出了Lambda表达式,彻底的简化了内部类操作
如创建一个类实现Runnable接口,需要这么复杂
public class Run implements Runnable {
@Override
public void run() {
System.out.println("run ----");
}
}
class TestRun{
public static void main(String[] args) {
Runnable runnable = new Run();
new Thread(runnable).start();
}
}
用匿名内部类实现:通过创建Run子类重写run方法实现
public class Run implements Runnable {
@Override
public void run() {
System.out.println("run ----");
}
}
class TestRun{
public static void main(String[] args) {
Runnable runnable = new Run(){
@Override
public void run(){
System.out.println("匿名内部类。。。");
}
};
new Thread(runnable).start();
}
}
通过Lambda表达式:
class TestRun{
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println("Run Lambda ---");
};
new Thread(runnable).start();
}
}
方法引用和构造器调用
前面,我们用Lambda表达式来实现匿名类方法。
但有些情况下,我们用Lambda表达式仅仅是调用一些已经存在的方法,除了调用动作外,没有其他任何多余的动作。
在这种情况下,我们倾向于通过方法名来调用它,Lambda表达式可以帮助我们实现这一要求,它使得Lambda在调用那些已经拥有方法名的方法的代码更简洁、更容易理解。
方法引用可以理解为Lambda表达式的另外一种表现形式
类型 | 语法 | 对应的Lambda表达式 |
---|---|---|
静态方法引用 | ClassName::staticMethod | (args) -> ClassName.staticMethod(args) |
实例方法引用 | inst::instMethod | (args) -> inst.instMethod(args) |
对象方法引用 | ClassName::instMethod | (inst,args) -> ClassName.instMethod(args) |
构造器引用 | ClassName::new | (args) -> new ClassName(args) |
::就是方法引用的操作符
最常用的就是System.out.println()方法
方法引用System.out::println
等价于lambda表达式
s -> System.out.println(s)
例如:
public interface Printable {
public void print(String string);
}
class PrintSimple {
public static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
//匿名内部类实现
Printable printable = new Printable() {
@Override
public void print(String string) {
System.out.println(string);
}
};
printString(printable);
}
}
Lambda表达式改进:
public static void main(String[] args) {
printString(s -> System.out.println(s));
}
方法引用:
public static void main(String[] args) {
printString(System.out::println);
}
特殊的构造器调用: String::new
等价与() -> new String()
举个例子:
interface IPrintC{
PrintC create();
}
class PrintC{
public PrintC() {
System.out.println("无参");
}
public PrintC(String s) {
System.out.println("有参:"+s);
}
}
class TestPrintC{
public static void main(String[] args) {
// 匿名内部类写法
/*IPrintC printC = new IPrintC() {
@Override
public PrintC create() {
return new PrintC();
}
};
*/
// lambda表达式写法
//IPrintC printC = () -> new PrintC();
// 构造器引用写法
IPrintC printC = PrintC::new;
printC.create();
}
}
单看方法引用比较摸不着头脑,从匿名内部类 -》lambda表达式 -》方法引用 一步步看,就比较清晰了
函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,可以被隐式转换为lambda表达式
我们前面的IPrintC接口就是函数式接口:
interface IPrintC{
PrintC create();
}
可以使用Lambda表达式来表示该接口的一个实现(JDK8前用一般是使用匿名内部类实现)
IPrintC printC = () -> new PrintC();
JDK8新增了一个包java.util.function,里面全是函数式接口,用来支持Java的函数式编程
Stream API
Stream流,可以让我们以一种声明的方式处理数据,用于集合的操作
在以前也写个Stream的文章:Stream流
什么是声明式的处理数据? 也就是类sql
这种风格把处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等
这个管道可以看成3节:创建Stream流 -》中间操作 -》结束操作
例如:list.stream()创建stream流,sorted().filter()为中间操作,foreach为结束操作
public static void main(String[] args) {
String[] str = {"Java","C#","Mysql","Python"};
List<String> list = Arrays.asList(str);
list.stream().sorted().filter(s -> s.length() > 3).forEach(System.out::println);
}
当3步都完成,才完成了Stream操作
接口支持静态方法和默认方法
- 在Java8以前,接口中只能有抽象方法(public abstract 修饰的方法)跟全局静态常量(public static final 常量 )
- 从Java8开始,允许接口中包含具有具体实现的default关键字修饰的方法,该方法称为 “默认方法”,接口中还允许添加静态方法
- default是在java8中引入的关键字,是指在接口内部包含了一些默认的方法实现,使得接口在进行扩展的时候,不会破坏与接口相关的实现类代码
interface TestI{
default void say(){
System.out.println("hello");
}
static void eat(){
System.out.println("eat");
}
}
类优先原则
如果一个类同时继承父类及实现接口,父类和接口中有相同方法
类的方法声明优先与接口默认方法
例如:现在有抽象类,我们添加构造方法以便判断
abstract class TestC{
public TestC() {
System.out.println("TestC 构造器");
}
abstract void say();
}
一个接口,有同名默认方法
public interface TestI{
default void say(){
System.out.println("hello TestI");
}
}
可以同时继承抽象类和实现接口
public class TestIC extends TestC implements TestI{
public static void main(String[] args) {
TestIC testIC = new TestIC();
testIC.say();
}
@Override
public void say() {
System.out.println("TestIC");
}
}
可以发现是继承的抽象类
那么能不能实现接口呢?
只需编写TestI.super.say();
public class TestIC extends TestC implements TestI{
public static void main(String[] args) {
TestIC testIC = new TestIC();
testIC.say();
}
@Override
public void say() {
TestI.super.say();
}
}
但实际上还是继承类,只不过是调用了接口的默认方法
提供默认方法是为了降低实现类之间的耦合,可以为接口提供默认方法而不需要破坏现有接口实现
改进的类型推导
泛型
在弄清类型推导前先了解一下泛型
泛型,即“参数化类型” ,即把操作的数据类型被指定为一个参数
List<String> list = new ArrayList<String>();
指定List中的数据为String类型
如果没有泛型:就需要强制类型转换
List list= new ArrayList();
list.add("Apple");
String s = (String) list.get(0);
参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法
- 泛型类
T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
类似与方法的形参
public class Store<T> {
private T t;
public Store(T t) {
this.t = t;
}
@Override
public String toString() {
return "Store{" +
"t=" + t +
'}';
}
}
实例化时泛型类时,必须指定T的具体类型
public class Test {
public static void main(String[] args) {
Store<String> store = new Store<String>("时间简史");
System.out.println(store);
}
}
- 泛型接口
与泛型类类型
interface Gen<T>{
T get();
}
- 泛型通配符
将泛型类作为形参的方法中,当我们不确定泛型时,可以用?通配符顶替
public void add(Store<?> store){
}
- 泛型方法
用<T>
表示这是一个泛型方法,就不用泛型类了
public class Store {
public<T> T get(T t){
return t;
}
}
Class<T>
类传递
当我们需要类时,确不清楚具体是哪个类,可以用Class<T>
表示
public <T> T getObject(Class<T> object) throws IllegalAccessException, InstantiationException {
T t = object.newInstance();
return t;
}
类型推导
泛型的最大优点是提供了程序的类型安全同时可以向后兼容,但是在JDK8前,使用泛型每次都要写明类型
两侧都要写明类型,明明感觉编辑器可以做到,但JDK7类型推导是有限制的:
只有构造器的参数化类型在上下文中被显著的声明了,才可以使用类型推断,否则不行
List<String> list = new ArrayList<String>();
JDK8改进的类型推导
List<String> list = new ArrayList<>();
越来越完善的类型推导就是完成了一些本来就感觉很理所当然的类型转换工作
Date
-
在JDK8前,日期时间API一直被开发者诟病,包括:java.util.Date是可变类型,SimpleDateFormat非线程安全等问题
-
JDK8引入了一套全新的日期时间处理API,新的API基于ISO标准日历系统
DateFormat的线程安全问题 :
-
有一个共享变量calendar,而这个共享变量的访问没有做到线程安全
-
当使用format方法时,实际是给calendar共享变量设置date值,然后调用subFormat将date转化成字符串
JDK8的Date API:
- LocalDate/LocalTime和LocalDateTime类可以在处理时区不是必须的情况
- 时区必须的情况: ZonedDateTime等类带时区的日期时间
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间
Optional
Optional类用于解决空指针异常,新版的SpringData Jpa和Spring Redis Data中都已实现了对该方法的支持
所以我们的数据库返回的结果集可以用Optional封装
例如:对于JPA的Repository接口,就可以设置返回类型为Optional
Optional<T> getById(Long id);
Optional类提供了这些方法:
如orElse:如果包装对象值非空,返回包装对象值,否则返回入参other的值
public static void main(String[] args) {
Optional<String> optionalS = Optional.empty();
String s = optionalS.orElse("empty");
System.out.println(s);
}
HashMap优化
JDK8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等
- 数据结构从 Hash表(数组+链表)改进成了Hash表+红黑树
因为当处理如果hash值冲突较多的情况下,链表的长度就会越来越长,此时通过单链表来寻找对应Key对应的Value的时候就会使得时间复杂度达到 O(n)
因此在JDK8之后,在链表新增节点导致链表长度超过 TREEIFY_THRESHOLD =
8 的时候,就会在添加元素的同时将原来的单链表转化为红黑树。
- 扩容机制
JDK7就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里,且是单链表的头插入方式
在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上
JDK8扩容采用2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置