嘘~ 正在从服务器偷取页面 . . .

Java 八股文


1. 基础概念

1.1 JVM、JDK、JRE

Java 虚拟机(Java Virtual Machine)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在

JDK(Java Development Kit)缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

JRE(Java Runtime Environment)是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。

关系示例图

1.2 字节码

Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。

Java 编译过程

1.3 Java 语言“编译与解释并存”

Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。后面引进了 JIT(just-in-time compilation) 编译器,当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。

1.4 Java 编码格式

为了显示其他语言,需要统一的 Unicode 编码,但是这会导致内存浪费(只需要一个字节的英文字符却占用四个字节)。

默认使用 UTF-8 优化内存占用,特点是对不同范围的字符使用不同长度的编码。

UTF-8 是变长字节表示,遵循一个转换的原则:

① 对于单字节的符号,字节的第一位设为0,后面的7位为这个符号的 Unicode 码,因此对于英文字母,UTF-8 编码和 ASCII 码是相同的;

② 对于 n 字节的符号(n>1),第一个字节的前 n 位都设为1,第 n+1 位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

而 JVM 内部采用 UTF-16(出现时间早于 UTF-8),所以 Java 中的 char 是两字节的占用。

2. Java 基础

2.1 面向对象三大特性

封装性,继承性,多态性。

封装:是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性,就是 Java 对象中 Setter 和 Getter。

继承:是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。

多态:表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

2.2 接口和抽象类

共同点:

  • 都不能被实例化;
  • 都可以包含抽象方法(在接口中,方法会默认修饰public abstract);
  • 都可以有默认实现的方法(Java 8 可以用 default 关键在接口中定义默认方法);

区别:

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系;
  • 一个类只能继承一个类,但是可以实现多个接口;
  • 接口中的成员变量只能是public static final,不能被修改且必须有初始值,而抽象类的成员变量默认default,可在子类中被重新定义,也可被重新赋值。

2.3 深拷贝和浅拷贝

图示区别

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象;
  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象;
  • 引用拷贝:引用指向同一个对象。

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。下面是浅拷贝:

public class Address implements Cloneable{
    private String name;
    // 省略构造函数、Getter & Setter 方法
    @Override
    public Address clone() {
        try {
            return (Address) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

public class Person implements Cloneable {
    private Address address;
    // 省略构造函数、Getter & Setter 方法
    @Override
    public Person clone() {
        try {
            Person person = (Person) super.clone();
            return person;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}


// test
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());

深拷贝:

@Override
public Person clone() {
    try {
        Person person = (Person) super.clone();
        // clone 的 address 之后引用不同地址
        person.setAddress(person.getAddress().clone());
        return person;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

// test
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());

2.3.1 Clone 替代

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

public class Dog {
    private final String name;
    private final int age;
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 使用拷贝构造函数进行深拷贝
    public Dog(Dog original) {
        this(original.name, original.age);
    }
    // 通过拷贝工厂 clone 新对象
    public static Dog newInstance(Dog dog) {
        return new Dog(dog.name, dog.age);
    }
}

2.4 String

2.4.1 String 不可变

String 字符串常量,在 Java 中设定不可变,源码中定义为 final。

  1. 提升使用效率(常量池的需求):

    如果大量的使用 String 常量,每一次声明一个 String 都创建一个 String 对象,将造成极大的空间浪费。Java 提出 String pool 的概念,在堆中开辟一块存储空间 String pool,当初始化一个 String 变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用,也可以提高 GC 的效率。

    由于 String 是不可变的,保证了 hashcode 的唯一性,于是在创建对象时其 hashcode 就缓存了,不需要重新计算。Map 将 String 作为 Key,处理速度要快过其它的键对象,所以键往往都使用 String。

  2. 避免值修改问题:

    如果采取可变的引用方式,对于一个值的修改会牵连到其他值,会导致安全问题:

public class test {
  // 不可变的 String
  public static String appendStr(String s) {
      s += "bbb";
      return s;
  }

  // 可变的 StringBuilder
  public static StringBuilder appendSb(StringBuilder sb) {
      return sb.append("bbb");
  }
  
  public static void main(String[] args) {
      String s = new String("aaa");
      // 常量池和堆内存中会分别分配一块内存
      String ns = test.appendStr(s);
      // s 的值会保持不变
      System.out.println("String aaa>>>" + s);
      // StringBuilder 做参数
      StringBuilder sb = new StringBuilder("aaa");
      StringBuilder nsb = test.appendSb(sb);
      System.out.println("StringBuilder aaa >>>" + sb.toString());
  }
}
  1. 多线程问题:

    在多线程情况中,读取不会有安全性问题。但是在写入的情况下,就可能会出现各种竞争的问题。设定为不可变之后,String 也可以直接存储 hash 值;

  2. 安全性问题:

    在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址 URL,文件路径 path,反射机制所需要的 String 参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值。因为String是不可变的,所以它的值是不可改变的。但由于String不可变,也就没有任何方式能修改字符串的值。

2.4.2 StringBuilder

StringBuffer 是线程安全的 StringBuilder 是不安全的(StringBuffer 是线程安全的,内部使用 synchronized 进行同步)。

StringBuilder 不支持并发操作,线性不安全的,不适合多线程中使用。新引入的 StringBuilder 类不是线程安全的,但其在单线程中的性能比 StringBuffer 高。

2.5 Java 传参

Java 的参数是以值传递的形式传入方法中,而不是引用传递。

当参数是对象时,传递的是对象的值,也就是对象的首地址。就是把对象的地址赋值给形参。

public class PassByValueExample {
    public static void main(String[] args) {
        Dog dog = new Dog("A");
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        func(dog);
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        System.out.println(dog.getName());          // A
    }

    private static void func(Dog dog) {
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        dog = new Dog("B");
        System.out.println(dog.getObjectAddress()); // Dog@74a14482
        System.out.println(dog.getName());          // B
    }
}

根据 StackOverflow 帖子中回答的解释,在函数中更改的是形参中的引用。但并没有对主函数中的 dog 产生改动,所以之后输出的 name 依然是 A。这不是引用传递,因为这并非是指向原来内容的指针。但如果修改引用对象内部的数据,原内部数据也会发生相应变化。

2.6 运算

在 Java 运算中,禁止隐式向下转化(double 不能转成 float,1.1f 才表示 float 类型)。

short x = 1;
// 不允许,3 默认是 int 类型,不能隐式转化
x = x + 3;
// 允许,相当于 x = (short) (x + 3)
x += 3;

2.6.1 位运算

在 Java 中,二进制表示的数值的最高位是符号位。

>>:表示有符号右移,会考虑到符号位的情况;

>>>:表示无符号左移,会使用0补全最高位。

public class Test {
    public static void main(String[] args) {
        int a = 20;
        int b = -20;
        System.out.println(a >> 2);
        System.out.println(b >> 2);
        System.out.println(a >>> 2);
        System.out.println(b >>> 2);
        /*  
            5 
            -5
            5
            1073741819
        */
    }
}

2.6.2 equals & hashCode

如果只重写 equals(),不重写 hashCode()。在理论情况下,如果 x.equals(y)== true,如果没有重写 equals 方法,那么这两个
对象的内存地址是同一个,意味着 hashCode 必然相等。有可能导致 hashCode 不相同。一旦出现这种情况,就导致这个类无法和所有集合类一起工作。

因为散列结合是使用 hashCode 来计算 key 的存储位置,如果存储两个完全相同的对象,但是有不同的 hashcode 就会导致这两个对象存储在 hash 表的不同位置。

2.7 static & final

static:

  1. 为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间;
  2. 使得某个方法或属性与类而不是对象关联在一起,即在不创建对象的情况下可通过类直接调用方法或使用类的属性;
  3. static 还能用来用在 import 中,这样使用变量和方法不需要指定 ClassName。

final:

用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。

申明的引用对象可以使引用不变,但是引用对象本身可以做出更改。

2.7.1 代码执行顺序

  1. 父类(静态变量、静态语句块);
  2. 子类(静态变量、静态语句块);
  3. 父类(实例变量、普通语句块);
  4. 父类(构造函数);
  5. 子类(实例变量、普通语句块);
  6. 子类(构造函数);
  7. 普通代码块。

2.8 try-catch

2.8.1 finally

finally 语句块在两种情况下不会执行:

  1. 在 try 或者cache 语句块中,执行了 System.exit(0)语句,导致 JVM 直接退出;
  2. 程序没有进入到 try 语句块因为异常导致程序终止,这个问题主要是开发人员在编写代码的时候,异常捕获的范围不够。

3. Java 进阶

3.1 Java 代理模式

需要进行代理的接口以及实现:

// 需要代理的接口
public interface SmsService {
    void send(String message);
}

public class SmsServiceImpl implements SmsService {
    @Override
    public void send(String message) {
        System.out.println("send message:" + message);
    }
}

3.1.1 静态代理

静态代理中,我们对目标对象的每个方法的增强都是手动完成的,接口一旦新增加方法,目标对象和代理对象都要进行修改且需要对每个目标类都单独写一个代理类。

// 静态代理类的写法
public class SmsProxy implements SmsService {
    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public void send(String message) {
        // 调用方法之前,我们可以添加自己的操作
        System.out.println("before method send()");
        smsService.send(message);
        // 调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method send()");
    }
}

// 主类直接调用代理类
public class Main {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsProxy smsProxy = new SmsProxy(smsService);
        smsProxy.send("java");
    }
}

3.1.2 动态代理

JDK 代理

Spring AOP、RPC 框架应该是两个不得不提的,它们的实现都依赖了动态代理。

  • 创建被代理的目标类以及其实现的接口;
  • 创建 InvocationHandler 接口的实现类,在 invoke() 中完成要代理的功能;
  • Proxy.newInstance() 动态地构造出代理对象。
// 实现处理类,处理 proxy 的方法实现
public class DebugInvocationHandler implements InvocationHandler {
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy,
                         Method method,
                         Object[] args) throws Throwable {
        System.out.println("before");
        Object result = method.invoke(target, args);
        System.out.println("after");
        return result;
    }
}

// 封装一个代理工厂类
public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 目标类的类加载
                target.getClass().getInterfaces(),  // 代理需要实现的接口,可指定多个
                new DebugInvocationHandler(target)   // 代理对象对应的自定义 InvocationHandler
        );
    }
}

public class Main {
    public static void main(String[] args) {
        SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
        smsService.send("java");
    }
}

CGLIB 代理

CGLIBopen in new window(Code Generation Library) 实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖。

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>
  1. JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外,CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法;
  2. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。JDK 代理使用的是反射机制实现 AOP 的动态代理,CGLib 代理使用字节码处理框架 ASM,通过修改字节码生成子类;
  3. JDK 动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托 hanlder 去调用原始实现类方法,CGLib 则使用的继承机制,具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。

3.2 Java 集合/容器

迭代器模式

Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。在 JDK 1.5 之后就可以通过 foreach 方法便利实现 Iterable 接口的聚合对象。

// 容器中使用适配器模式,Arrays.asList 中可以
// asList 的参数为泛型的变长参数
Integer[] arr = {1, 2,3 };
List list = Arrays.asList(arr);

List list = Arrays.asList(1, 2, 3);

3.2.1 Java List

ArrayList & Vector

  • ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
  • VectorList 的古老实现类,底层使用Object[ ] 存储,线程安全的(Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量)。

Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环),两者都不是线程安全的。

建议项目中不要使用 LinkedList(基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列),下面是 LinkedList 作者的推特内容:

LinkedList 开发者在 twitter 上的回答

Vector 和 HashTable 一样,内部使用 synchronized 保证安全,性能和并发效率较低。这几种 List 在迭代期间不允许编辑,如果在迭代期间进行添加或删除元素等操作,则会抛出 ConcurrentModificationException 异常。

CopyOnWrite

从 JDK 1.5 开始,Java 并发包里提供了使用 CopyOnWrite 机制实现的并发容器 CopyOnWriteArrayList 作为主要的并发 List,CopyOnWrite 的并发集合还包括 CopyOnWriteArraySet,其底层正是利用 CopyOnWriteArrayList 实现的。

CopyOnWriteArrayList 适用于读多写少的场景。CopyOnWriteArrayList 读取不加锁,写入也不会阻塞读取操作在写入的同时进行读取。只有写入和写入之间需要进行同步。

CopyOnWrite 的含义在于,当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器,完成修改之后,再将原容器的引用指向新的容器。这样就完成了整个修改过程。因为这个不变性,所以读取的过程就不需要加锁同步(Collections.synchronizedList() 也能得到一个线程安全的 ArrayList)。

public class CopyOnWriteArrayListDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 3});
        System.out.println(list); // [1, 2, 3]

        // Get iterator 1
        Iterator<Integer> itr1 = list.iterator();
        // Add one element and verify list is updated
        list.add(4);
        System.out.println(list); // [1, 2, 3, 4]

        // Get iterator 2
        Iterator<Integer> itr2 = list.iterator();
        System.out.println("====Verify Iterator 1 content====");
        itr1.forEachRemaining(System.out::println); // 1,2,3
        System.out.println("====Verify Iterator 2 content====");
        itr2.forEachRemaining(System.out::println); // 1,2,3,4
    }
}
缺点
  • CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,这一点会占用额外的内存空间。复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,会降低整体性能;
  • 由于 CopyOnWrite 容器的修改是先修改副本,所以这次修改对于其他线程来说,并不是实时能看到的,只有在修改完之后才能体现出来。
源码分析

以 add 方法为例:

public boolean add(E e) {
    // 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 得到原数组的长度和元素
        Object[] elements = getArray();
        int len = elements.length;
        // 复制出一个新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 添加时,将新元素添加到新数组中
        newElements[len] = e;
        // 将volatile Object[] array 的指向替换成新数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

在添加的时候首先上锁,并复制一个新数组,增加操作在新数组上完成,然后将 array 指向到新数组,最后解锁。

而在迭代器 COWIterator 中,有两个重要的属性,分别是 Object[] snapshot 和 int cursor。其中 snapshot 代表数组的快照,cursor 则是迭代器的游标。

private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}

迭代器在被构建的时候,会把当时的 elements 赋值给 snapshot,而之后的迭代器所有的操作都基于 snapshot 数组进行。

ArrayList 扩容机制

ArrayList 是基于数组实现,RandomAccess 接口标识着该类支持快速随机访问。数组的默认大小为 10。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

/**
     * Constructs an empty list with an initial capacity of ten.
     * ArrayList():并不是在初始化 ArrayList 的时候就设置初始容量为空,而是在添加第一个元素时,将初始容量设置为10(DEFAULT_CAPACITY)。
     * 在 JDK 1.6 中,无参构造方法的初始容量为 10
     */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),即 oldCapacity + oldCapacity / 2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍左右。

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 在源码中可以看到,每次需要扩容的时候,新数组都会扩展到原数组的 1.5 倍
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

transient 分析

ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    // 数组默认不会被序列化
    transient Object[] elementData;

    // 在这里的分配大小选择了 Int 最大长度 - 8
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
        // Write out size as capacity for behavioral compatibility with clone()
        s.writeInt(size);
        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();
        // Read in capacity
        s.readInt(); // ignored
        if (size > 0) {
            // like clone(), allocate array based upon size not capacity
            SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
            Object[] elements = new Object[size];
            // Read in all elements in the proper order.
            for (int i = 0; i < size; i++) {
                elements[i] = s.readObject();
            }
            elementData = elements;
        } else if (size == 0) {
            elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new java.io.InvalidObjectException("Invalid size: " + size);
        }
    }
}

elementData 是一个缓存数组,它通常会预留一些空间,比如初始化添加第一个元素的时候,容量是 10,只有当存第 11 个元素的时候,才会扩容,所以存 1~10 个元素的时候,容量都是 10,所以并不是所有空间都有元素。因此,整个 ArrayList 重写序列化和反序列化方法,保证只序列化到实际储存的元素,而不是整个数组,避免了不必要的浪费。

序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);

Fail-Fast

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。

3.2.2 Java Set

冷知识:Java 的 Set 底层实现还是 Map。

HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同:

  • HashSet 的底层数据结构是哈希表(基于 HashMap 实现,使用 Iterator 遍历得到的结果是不确定的)。

  • LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO(先进先出原则)。

  • TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序(查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN))。

在 openjdk8 中,实际上无论HashSet中是否已经存在了某元素,HashSet都会直接插入,只是会在add()方法的返回值处告诉我们插入前是否存在相同元素

3.2.3 Java Map

在 Java 中,HashMap 使用拉链法解决哈希冲突问题(新冲突的数据会插入链表的头部)。

HashMap

HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。

HashMap 总是使用 2 的幂作为哈希表的大小。因为哈希函数的计算方法为 (n - 1) & hash,在这个前提下hash % length == hash & (length-1)。由于 & 的运算效率大于 %,这样对性能也有一定的提升。

JDK 1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间(使用红黑树避免出现二叉查找树的线性结构的问题)。

数据结构转换图示

对于负载因子的选择,Java 底层采取了 0.75,涉及到数学中的泊松分布。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少扩容 rehash 操作次数,所以,一般在使用 HashMap 时建议根据预估值设置初始容量,以便减少扩容操作。选择 0.75 作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择。

在多线程情况下使用 HastMap 可能会出现循环链表的情况(在线程中一和线程二中,节点分别指向了之后的节点,形成一个闭环):

循环链表图示

对于 HashMap 的遍历问题,详情参考这篇文章的内容。结论来说,不能在遍历中使用集合 map.remove() 来删除数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的删除集合的方式。

从性能来说,在获取迭代器中EntrySetKeySet 的性能高。KeySet 在循环时使用了 map.get(key),而 map.get(key) 相当于又遍历了一遍 Map 集合去查询 key 所对应的值。在使用迭代器或者 for 循环时,已经遍历了一遍 Map 集合了,因此再使用 map.get(key) 查询时,相当于遍历了两遍

EntrySet 只遍历了一遍 Map 集合,之后通过代码Entry<Integer, String> entry = iterator.next()把对象的 keyvalue 值都放入到了 Entry 对象中,因此再获取 keyvalue 值时就无需再遍历 Map 集合。

EntrySet 的性能比 KeySet 的性能高出了一倍,因为 KeySet 相当于循环了两遍 Map 集合,而 EntrySet 只循环了一遍。

public static void entrySet() {
    Iterator var0 = map.entrySet().iterator();
    while(var0.hasNext()) {
        Entry var1 = (Entry)var0.next();
        System.out.println(var1.getKey());
        System.out.println((String)var1.getValue());
    }
}
public static void forEachEntrySet() {
    Iterator var0 = map.entrySet().iterator();
    while(var0.hasNext()) {
        Entry var1 = (Entry)var0.next();
        System.out.println(var1.getKey());
        System.out.println((String)var1.getValue());
    }
}

HashMap 本质并非安全的方法,在源码种会涉及 count++、同时 put、死锁等等问题。

扩容

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此查找的复杂度为 O(N/M)。

为了让查找的成本降低,应该使 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。

参数 含义
capacity table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。
size 键值对数量。
threshold size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。
loadFactor 装载因子,table 能够使用的比例,threshold = (int)(capacity * loadFactor)。
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;

// 当需要扩容时,令 capacity 为原来的两倍。
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

// 扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,这一步是很费时
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

HashTable

  • 不允许键值为 null;
  • put 方法使用 sychronized 方法进行线程同步。

单线程无需同步,多线程可用 concurrent 包的类型,Vector 也有类似的问题,所以两者都已经不建议使用(遗留类)。

Hashtable 全表锁

HashTable 和 HashMap

从功能特性的角度来说 HashTable 是线程安全的,而 HashMap 不是。HashMap 的性能要比HashTable 更好。因为 HashTable 采用了全局同步锁来保证安全性,对性能影响较大。

从内部实现的角度来说 HashTable 使用数组 + 链表、HashMap 采用了数组 + 链表 + 红黑树

HashMap 初始容量是 16、HashTable 初始容量是 11。HashMap 可以使用 null 作为 key,HashMap 会把 null 转化为 0 进行存储,而 Hashtable 不允许。

最后,他们两个的 key 的散列算法不同,HashTable 直接是使用 key 的 hashcode 对数组长度做取模。而 HashMap 对 key 的hashcode 做了二次散列,从而避免key 的分布不均匀问题影响到查询性能。

ConcurrentHashMap

在 JDK 1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。

static final class Segment<K,V> extends ReentrantLock implements Serializable Segment 继承自ReentrantLock。

内部 Segment 继承 ReentrantLock 可以理解为一把锁。各个 Segment 之间都是相互独立上锁的,互不影响。相比于之前的 Hashtable 每次操作都需要把整个对象锁住而言,大大提高了并发效率。

JDK 1.7 ConcurrentHashMap

JDK 1.8 之后 ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全(在 CAS 操作失败时使用内置锁 synchronized)。数据结构跟 HashMap 1.8 的结构类似,数组 + 链表/红黑二叉树。synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发。

JDK 1.8 ConcurrentHashMap

图中的节点有三种类型:

  1. 空着的位置表示还没有元素进行填充;
  2. 和 HashMap 非常类似的拉链法结构,在每一个槽中会首先填入第一个节点,但是后续如果计算出相同的 Hash 值,就用链表的形式往后进行延伸;
  3. 红黑树结构,这是 Java 7 的 ConcurrentHashMap 中所没有的结构。

在这样的结构中,链表长度大于 8 就会转换成红黑树;如果节点数小于 6,又会回退到链表。

JDK 的源码注释中对这个问题的解释:

Because TreeNodes are about twice the size of regular nodes, use them only when bins contain enough nodes to warrant use(see TREEIFY_THRESHOLD). And when they become too small (due removal or resizing) they are converted back to plain bins.

单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD 的值决定的。而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间。

In usages with well-distributed user hashCodes, tree bins are rarely used.  Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:

 0:0.60653066
 1:0.30326533
 2:0.07581633
 3:0.01263606
 4:0.00157952
 5:0.00015795
 6:0.00001316
 7:0.00000094
 8:0.00000006

 more: less than 1 in ten million
如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。
null 问题

为什么 ConcurrentHashMap 不允许 key 或者 value 为空呢?为了避免在多线程环境下出现歧义问题。

如果 key 或者 value 为 null,当我们通过 get(key) 获取对应的 value 的时候,如果返回的结果是 null 我们没办法判断,它是put(k,v) 的时候,value 本身为 null 值,还是这个 key 本身就不存在。

比如在这样一种情况下,线程 t1 调用 containsKey 方法判断 key 是否存在,假设当前这个 key 不存在,本来应该返回 false。但是在 T1 线程返回之前,正好有一个T 2 线程插入了这个 key,但是 value 为null。这就导致原本 T1 线程返回的结果有可能是 true,有可能是 false,取决于 T1 和 T2 线程的执行顺序。

HashMap 允许存储 null 自然是因为不是线程安全集合。

HashTree

TreeMapHashMap 都继承自AbstractMap ,在这之外TreeMap实现了NavigableMap接口和SortedMap 接口。

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。

实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力(默认是按 key 的升序排序)。

TreeMap 重写排序示例:

public class Person {
    private final Integer age;

    public Person(Integer age) {
        this.age = age;
    }

    public Integer getAge() {
        return age;
    }

    public static void main(String[] args) {
        // 通过 age 进行升序排序
        TreeMap<Person, String> treeMap = new TreeMap<>(Comparator.comparing(Person::getAge));
        treeMap.put(new Person(3), "person1");
        treeMap.put(new Person(18), "person2");
        treeMap.put(new Person(35), "person3");
        treeMap.put(new Person(16), "person4");
        treeMap.forEach((k, v) -> System.out.println(k.getAge() + ":" + v));
    }
}

LinkedHashMap

继承自 HashMap,因此具有和 HashMap 一样的快速查找特性(public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>)。内部维护了一个双向链表,用来维护插入顺序或者 LRU(Least Recently Used) 顺序。

/**
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> head;
/**
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> tail;
// accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序
final boolean accessOrder;
afterNodeAccess()

当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
afterNodeInsertion()

在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。evict 只有在构建 Map 的时候才为 false,在这里为 true。

removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
LRU 缓存
  • 设定最大缓存空间 MAX_ENTRIES 为 3;
  • 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
  • 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_ENTRIES = 3;

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }

    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}

3.2.4 Collection

Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。

Collections 是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法:sort(list);提供的反转方法:reverse(list)。

可以类比 Array 和 Arrays。

3.3 Java 序列化

将存放在内存中的数据转移到硬盘中的过程,需要用到序列化(比如 ORM 框架)。

  • 序列化: 将数据结构或对象转换成二进制字节流的过程;
  • 反序列化:将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程。
  1. 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  2. 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  3. 将对象存储到缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

Java 中的序列化需要实现 Serializable 接口,反序列化时需要提供无参的构造函数,不然会报错。

通过ObjectOutputStream&ObjectInputStream实现这一系列操作,分别调用writeObject(Object obj)&readObject()方法。

序列化过程示意图

3.3.1 序列化方式

原生 JDK 序列化方式存在性能差(序列化之后的字节数组体积较大,导致传输成本加大),不支持跨语言调用。

Kryo 是高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。在引入其对应依赖包后,就可以使用序列化。

3.3.2 注意点

  1. 静态成员变量不能序列化,序列化针对对象属性,而静态成员变量属于类;

  2. 在序列化时添加一个serialVersionUID。Java 会将传来的 UID 和本地的 UID 进行比对,如果不一致,不同意反序列化操作。

    导致 UID 兼容性问题可能有这些原因:

    • 手动修改导致当前的 serialVersionUID 与序列化前的不一样;
    • 缺少 serialVersionUID 常量,JVM 内部会根据类结构去计算 serialVersionUID,在类结构发生改变时(属性增加,删除或者类型修改了)导致 serialVersionUID 发生变化。
    • 假如类结构没有发生改变,并且没有定义 serialVersionUID ,虚拟机不一样也可能导致 serialVersionUID 不一样。

3.3.3 transient

transient 关键字的主要作用就是让某些被 transient 关键字修饰的成员属性变量不被序列化。导致被 transient 修饰的字段会重新计算,初始化。

比如:如果一个用户有一些密码等信息,为了安全起见,不希望在网络操作中被传输,这些信息对应的变量就可以加上 transient 关键字

3.4 Java 反射

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及 动态调用对象的方法的功能称为 Java 语言的反射机制。

反射优点:

  • 增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作;
  • 提高代码的复用率,比如动态代理,就是用到了反射来实现;
  • 可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用

反射的缺点如下:

  • 性能瓶颈:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。代码可读性也会下降;

  • 安全问题:使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了;

  • 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

反射使用:

  • 我们在使用 JDBC 连接数据库时使用 Class.forName() 通过反射加载数据库的驱动程序;
  • Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
  • 动态配置实例的属性。

3.5 泛型

通用的泛型标准:

  • E-Element 在集合中使用,表示在集合中存放的元素;
  • T-Type 表示Java类;
  • K-Key 表示键;
  • V-Value 表示值;
  • N-Number 表示数值类型;
  • ? 表示不确定的类型,是类型实参,不是类型形参。

对泛型的上限限定:<? extends T>,表示该通配符所代表的类型是T的子类或是T的实现类;

对泛型的下限限定:<? super T>,表示该通配符所代表的类型是T的父类或父接口。

public class GenericTest {
    public class Generic<T> {
        private final T key;

        public Generic(T key) {
            this.key = key;
        }

        public T getKey() {
            return key;
        }

        /*
        修饰符和返回值之间的 <T, K> 必不可少,表示泛型方法并申明泛型类型
        (因为类已经申明 T 类型,所以 <T> 可以省略)
        public <T, K> K showKeyName(Generic<T> container) {
             ...
        }
        */
        
        public T showKeyName(Generic<T> container) {
            return container.getKey();
        }
    }
}

3.5.1 类型擦除

但是 Java 的泛型并不是真正的泛型,在编译时,泛型中的类型会被擦除,在运行时不存在任何类型相关的信息。例如 List<String>在运行时仅用一个 List 来表示。为了确保能和 Java 5 之前的版本开发二进制类库进行兼容。

public static void main(String[] args) {
    ArrayList<String> arrayString = new ArrayList<String>();
    ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
    // output true
    System.out.println(arrayString.getClass()==arrayInteger.getClass());
}

3.5.2 面试题

List<String> 并不能传递给 List<Object>,因为 Object 范围更宽,这样传递会导致编译错误。

Array 并不支持泛型,Effective Java 书中建议使用 List 来代替 Array。利用 List 的泛型可以保证编译期间的类型安全保证。

如果把泛型和原始类型混合起来使用,例如下列代码,Java 5 的 javac 编译器会产生类型未检查的警告,例如 List<String> rawList = new ArrayList()

3.6 Java 封装类型

在 Java 里面,之所以要对基础类型设计一个对应的封装类型。是因为 Java 本身是一门面向对象的语言,对象是 Java 语言的基础单元,我们时时刻刻都在创建对象,也随时都在使用对象,很多时候在传递数据时也需要对象类型,比如像 ArrayList、HashMap 这些集合,只能存储对象类型,因此从这个点来说,封装类型存在的意义就很大。

基本类型和 Integer 类型混合使用时,Java 会自动通过拆箱和装箱实现类型转换 Integer 作为一个对象类型,封装了一些方法和属性,我们可以利用这些方法来操作数据。

封装类型还有很多好处,比如:

  • 安全性较好,可以避免外部操作随意修改成员变量的值,保证了成员变量和数据传递的安全性;
  • Integer 存储在堆内存,int 类型是直接存储在栈空间;
  • 隐藏了实现细节,对使用者更加友好,只需要调用对象提供的方法就可以完成对应的操作。

3.6.1 IntegerCache

Integer a1 = 100、Integer a2 = 100,请问 a1==a2 的运行结果以及为什么?(结果为 true)

Integer a1=100, 把一个 int 数字赋值给一个封装类型,Java 会默认进行装箱操作,调用 Integer.valueOf() 方法,把数字 100 包装成封装类型 Integer。

在 Integer 内部设计中,用到了享元模式的设计,享元模式的核心思想是通过复用对象,减少对象的创建数量,从而减少内存占用和提升性能。Integer 内部维护了一个 IntegerCache,它缓存了 -128 到 127 这个区间的数值对应的 Integer 类型。一旦程序调用valueOf 方法,如果数字是在 -128 到 127 之间就直接在 cache 缓存数组中去取 Integer 对象。

JDK 关于 IntegerCache 的源码

3.7 设计模式

3.7.1 单例模式

单例模式,就是一个类在任何情况下绝对只有一个实例,并且提供一个全局访问点来获取该实例。使用最频繁的设计模式之一,为了防止浪费资源。

加锁

懒汉式加载,最初不进行初始化,在第一次调用时才进行初始化,之后返回初始化后的结果。问题也比较明显,没法避免多线程问题。

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

改进的方式是通过 synchronized 进行加锁处理。但是加锁会出现性能问题,而且锁指需要在第一次初始化中有用。

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

使用双锁是为了解决单例模式在多线程环境下的并发问题。当多个线程同时访问 getInstance() 方法时,如果单例实例还未创建,那么多个线程会同时进入 if (INSTANCE == null) 这个判断语句,导致多个线程同时创建了多个实例,违背了单例模式的定义。

需要注意的是,使用双重检查锁时需要将单例变量设置为 volatile,确保线程间的可见性和防止指令重排。

public class Singleton {
    private static volatile Singleton INSTANCE;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

静态内部类

比较简单的实现方式是饿汉式,不管是否调用都会初始化。因为是静态块里初始化,所以只会执行一次。

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

饿汉式的问题在于无论何时都会进行加载,这会可能会导致资源无意义消耗。我们可以把 INSTANCE 写在静态内部类中,静态内部类只有在调用时被加载,这样实现了延时加载。

public class Singleton {
    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

枚举

  1. 线程安全:枚举类默认就是线程安全的,不需要再使用同步锁或者 volatile 关键字;
  2. 防止反序列化:枚举类的实例是在类加载时创建的,并且是 final 类型,不能被反序列化创建新的对象;
  3. 防止反射攻击:枚举类中没有公开的构造函数,不能使用反射创建新的对象。
public enum Singleton {
    INSTANCE;
    public void someMethod() {
        // do something
    }
}

4. Java 多线程

多线程大三特性:

  • 原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行;
  • 可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值;
  • 有序性:程序执行的顺序按照代码的先后顺序执行

4.1 线程死锁

死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用;
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源;
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

预防死锁:

  1. 破坏请求与保持条件 :一次性申请所有的资源;
  2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源;
  3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

4.2 sleep() & wait()

sleep() 方法没有释放锁,而 wait() 方法释放了锁

  • 两者都可以暂停线程的执行;
  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行;
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

4.3 start() & run()

为什么调用 start() 才能开始多线程,而直接使用 run() 不行呢?

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它。

调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

4.4 synchronized & ReentrantLock

两者都是可重入锁。“可重入锁”指的是自己可以再次获取自己的内部锁。

比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API(需要配合 try catch & lock() unlock() 使用)。

ReentrantLock 提供了更安全的方法以及更多的功能。

4.5 volatile

在 JDK 1.5 中,java.util.concurrent(简称 JUC),增加了并发编程使用的工具类。

通过 AtomicBoolean 、AtomicInteger 、AtomicLong 和 AtomicReference 提供原子性支持。

5. JDK 特性

5.1 JDK 1.8

JDK 1.8,第一个 Java 更新的大版本。正式实现了 Lambda 表达式以及 LocalDate/LocalDateTime。对底层内容进行不少的优化。

5.1.1 interface

新 interface 的方法可以用defaultstatic修饰,这样就可以有方法体,实现类也不必重写此方法。

  • default修饰的方法,是普通实例方法,可以用this调用,可以被子类继承、重写;

  • static修饰的方法,使用上和一般类静态方法一样。但它不能被子类继承,只能用Interface调用。

5.1.2 Lambda

可以使用方法引用来代替某些 Lambda 表达式。

lambda 表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在lambda内部修改定义在域外的变量。

public class Test {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c", "d", "e");
        list.forEach(System.out::println);
//        list.forEach((String s) -> System.out.println(s));
        // 编译器会推断类型

        List<Integer> strings = Arrays.asList(1, 2, 3);

        /*strings.sort(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });*/
        strings.sort(Comparator.comparingInt(o -> o));
    }
}

使用例子

public static void main(String[] args) {
    Supplier<Student> a = Student::new;// 构造引用
    a.get().setName("cxy");
    System.out.println(a);
    // Supplier<String> b = () -> "Hello, World";

}

Java Supplier 是一个功能接口,代表结果的提供者。

一个 Supplier 可以通过 lambda 表达式、方法引用或默认构造函数来实例化。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();// 对象:实例方法
    list.forEach(System.out::println);
    // list.forEach(o -> {System.out.println(o);});

    Stream<Double> stream = Stream.generate(Math::random);
}

Stream

jdk 1.8 正式引入 stream 操作,通过时间换空间的方式,简化开发操作。stream 不用对原本的数据进行修改。

filter

filter 是对数据进行过滤,主要用于筛选特定数值的数据。

Predicate 直译是谓词,这里表示用于 filter 的条件参数,用于自定义过滤条件。在源码中,Predicate 也提供了 or、and 等方法形成多条件过滤。

public static void main(String[] args) {
    List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

    System.out.println("Languages which starts with J :");
    filter(languages, (str) -> str.startsWith("J"));

    System.out.println("Languages which ends with a ");
    filter(languages, (str) -> str.endsWith("a"));

    System.out.println("Print all languages :");
    filter(languages, (str) -> true);

    System.out.println("Print no language : ");
    filter(languages, (str) -> false);

    System.out.println("Print language whose length greater than 4:");
    filter(languages, (str) -> str.length() > 4);
}

public static void filter(List<String> names, Predicate<String> condition) {
    names.stream().filter(condition).forEach((name) -> {
        System.out.println(name + " ");
    });
}
map & Collectors

map 将集合类(例如列表)元素进行转换的,最常用的有 mapToInt 这种可以让 List 中的 Integer 转化为 int 执行一些操作。

public static void main(String[] args) {
    List<Integer> costBeforeTax = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    // 通过 lambda 表达式计算总花费,使用 reduce 方法将所有值合并
    double bill = costBeforeTax.stream().map((i) -> i + 0.12 * i).reduce(Double::sum).get();
    // 61.599999999999994
    System.out.println(bill);

    List<String> G7 = Arrays.asList("USA", "Japan", "France", "Germany", "Italy", "UK", "Canada");
    String total = G7.stream().map(String::toUpperCase).collect(Collectors.joining(", "));
    // String total = G7.stream().map(String::toUpperCase).reduce("", (s, s2) -> s + ", " + s2);
    // , USA, JAPAN, FRANCE, GERMANY, ITALY, UK, CANADA(reduce 中的参数为初始值)
    System.out.println(total);
    // USA, JAPAN, FRANCE, GERMANY, ITALY, UK, CANADA
    
    /*	Collectors.joining(", ")
		Collectors.toList()
		Collectors.toSet() ,生成 set 集合
		Collectors.toMap(MemberModel::getUid, Function.identity())
		Collectors.toMap(ImageModel::getAid, o -> IMAGE_ADDRESS_PREFIX + o.getUrl())*/
}

对于 reduce 的返回值是 Optional,是防止 stream 中的内容为空。

Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent()) {
    System.out.println(opt.get());
}
peek

peek 作为调试用的方法,类似于在 stream 的过程中打断点。

对于 peek,可以只使用一个空的方法体,但是有些 IDE 可能不允许为空,可以通过输出解决问题。

List<Person> lists = new ArrayList<Person>();
lists.add(new Person(1L, "p1"));
lists.add(new Person(2L, "p2"));
lists.add(new Person(3L, "p3"));
lists.add(new Person(4L, "p4"));
System.out.println(lists);

List<Person> list2 = lists.stream()
				 .filter(f -> f.getName().startsWith("p"))
                .peek(t -> {
                    System.out.println(t.getName());
                })
                .collect(Collectors.toList());
System.out.println(list2);
其他
  • distinct:去除重复数值;

  • count:统计元素个数;

  • flatMap:将多个 Stream 进行连接;

    List<Integer> result= Stream.of(Arrays.asList(1,3),Arrays.asList(5,6))
        .flatMap(a->a.stream()).collect(Collectors.toList());
  • max/min:可以通过重写比较器实现复杂逻辑的计算;

    Person a = lists.stream().min(new Comparator<Person>() {
    
        @Override
        public int compare(Person o1, Person o2) {
            if (o1.getId() > o2.getId()) return -1;
            if (o1.getId() < o2.getId()) return 1;
            return 0;
        }
    }).get();
    
    //获取数字的个数、最小值、最大值、总和以及平均值
    List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
    IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics();
    System.out.println("Highest prime number in List : " + stats.getMax());
    System.out.println("Lowest prime number in List : " + stats.getMin());
    System.out.println("Sum of all prime numbers : " + stats.getSum());
    System.out.println("Average of all prime numbers : " + stats.getAverage());
    
  • Match:类似 filter,但返回布尔值类型;

    boolean anyStartsWithA =
        stringCollection
            .stream()
            .anyMatch((s) -> s.startsWith("a"));
    
    System.out.println(anyStartsWithA);      // true
    
    boolean allStartsWithA =
        stringCollection
            .stream()
            .allMatch((s) -> s.startsWith("a"));
    
    System.out.println(allStartsWithA);      // false
    
    boolean noneStartsWithZ =
        stringCollection
            .stream()
            .noneMatch((s) -> s.startsWith("z"));
    
    System.out.println(noneStartsWithZ);      // true

5.1.3 默认方法

在 JDK 1.8 中 接口可以有实现方法,而且不需要实现类去实现其方法。只需在方法名前面加个 default 关键字即可。

interface HumanActivity {
    default void run() {
        System.out.println("He can run!");
    }
}

至于为何添加默认方法,主要是为了在添加新的接口时,不用实现接口中定义的所有方法。JDK 1.8 之前的集合框架没有 forEach 方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。

5.1.4 Optional

开发中,我们需要预防 NPE(java.lang.NullPointerException)问题。

(1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE;

(2) 数据库的查询结果可能为 null;

(3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null;

(4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE;

(5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针;

(6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。

Optional<String> a = Optional.of("123");// 正常 Optional 不允许值为空
// 使用 ofNullable 表示可以传入 null
// ifPresent 用于判断值是否存在
// 在最后使用 get() 获得实例的值

Zoo zoo = getZoo();
if(zoo != null){
   Dog dog = zoo.getDog();
   if(dog != null){
      int age = dog.getAge();
      System.out.println(age);
   }
}
// 对于冗长的连续空值判读,我们可以简化成以下代码块
// 这里使用方法引用,也可以使用 lambda 表达式
// 使用 map 内容对应 function
Optional.ofNullable(zoo).map(Zoo::getDog).map(Dog::getAge).ifPresent(System.out::println);
// ifPresent 用于判断值存在之后的操作

Optional 常用方法:

Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).filter(v->v==1).orElse(3);

// orElse() 不管当前 value 是否为空都会执行
// orElseGet() 懒加载形式,只有为 null 的情况才会调用 Get(推荐使用)
// System.out.println(empty.orElseGet(() -> "Default Value"));
// orElseThrow() 例子:empty.orElseThrow(ValueAbsentException::new);
// 如果为空抛出对应异常

// filter 与 stream 用法类似
Optional.ofNullable(zoo).map(Zoo::getDog).map(Dog::getAge).filter(age -> age > 10).ifPresent(System.out::println);

// map 也提供类型转换,map 方法中的 lambda 表达式返回值可以是任意类型,在 map 函数返回之前会包装为 Optional
// flatMap 方法中的 lambda 表达式返回值必须是 Optionl 实例
// map 方法用于将 Optional 对象的值转换为另一种类型,而 flatMap 方法用于将 Optional 对象的值转换为另一个 Optional 对象
Optional<Dog> dogOptional = Optional.of(dog);
Optional<String> o = dogOptional.map(Dog::getName);
Optional<String> u = dogOptional.flatMap(d -> Optional.of(d.getName()));

5.2 JDK 11

JDK 9 已经允许在接口中使用私有方法。

JDK 10 之后引入了局部变量推断 var,JDK 11 之后允许开发者 Lambda 表达式中使用 var 进行参数声明。看似在 Lambda 中使用参数类型没有作用,因为 Lambda 使用隐式类型定义。添加上类型定义同时使用 @Nonnull 和 @Nullable 等类型注释还是很有用的,既能保持与局部变量的一致写法,也不丢失代码简洁。

@Nonnull var x = new Foo();
(@Nonnull var x, @Nullable var y) -> x.process(y)

5.2.1 新工具和库更新

在集合上,Java 9 增加 了 List.of()Set.of()Map.of()Map.ofEntries()等工厂方法来创建不可变集合 (这点类似 kotlin 创建数组的写法)。

List.of();
List.of("Hello", "World");
List.of(1, 2, 3);
Set.of();
Set.of("Hello", "World");
Set.of(1, 2, 3);
Map.of();
Map.of("Hello", 1, "World", 2);

Stream 中增加了新的方法 ofNullable、dropWhile、takeWhile 和 iterate。

var count = Stream.of(1, 2, 3, 4, 5)
    .dropWhile(i -> i % 2 != 0)
    .toArray();
for (var i : count) {
    System.out.println("i = " + i);
}
/*
    i = 2
    i = 3
    i = 4
    i = 5
*/

Collectors 中增加了新的方法 filtering 和 flatMapping。Optional 类中新增了 ifPresentOrElse、or 和 stream 等方法(stream 可以返回对应的 stream 流)。

var count1 = Stream.of(
    Optional.of(1),
    Optional.empty(),
    Optional.of(2)
).flatMap(Optional::stream).toArray();
for (var i : count1) {
    System.out.println("i = " + i);
}

5.2.2 进程 API

ava 9 增加了 ProcessHandle 接口,可以对原生进程进行管理,尤其适合于管理长时间运行的进程。在使用 ProcessBuilder 来启动一个进程之后,可以通过 Process.toHandle() 方法来得到一个 ProcessHandle 对象的实例。通过 ProcessHandle 可以获取到由 ProcessHandle.Info 表示的进程的基本信息,如命令行参数、可执行文件路径和启动时间等。ProcessHandle 的 onExit()方法返回一个 CompletableFuture对象,可以在进程结束时执行自定义的动作。

final ProcessBuilder processBuilder = new ProcessBuilder("top")
    .inheritIO();
final ProcessHandle processHandle = processBuilder.start().toHandle();
processHandle.onExit().whenCompleteAsync((handle, throwable) -> {
    if (throwable == null) {
        System.out.println(handle.pid());
    } else {
        throwable.printStackTrace();
    }
});

5.2.3 简化启动

JDK 9 中新增 jshell,可以用于执行 Java 代码并立即获得执行结果。支持定义变量、方法、类等,支持输入语句、表达式,支持导入外部 Java 源文件(类似 Python 直接执行输入代码)。

jshell 例子

Java 11 版本中增强了 Java 启动器,使之能够运行单一文件的 Java 源代码。此功能允许使用 Java 解释器直接执行 Java 源代码。源代码在内存中编译,然后由解释器执行。唯一的约束在于所有相关的类必须定义在同一个 Java 文件中(可以直接通过 java *.java 文件完成编译)。

5.2.4 安全性提升

Java 9 新增了 4 个 SHA-3 哈希算法,SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。另外也增加了通过 java.security.SecureRandom 生成使用 DRBG 算法的强随机数。

根证书认证 & TLS

自 Java 9 起在 keytool 中加入参数 -cacerts ,可以查看当前 JDK 管理的根证书。而 Java 9 中 cacerts 目录为空,从 Java 10 开始,将会在 JDK 中提供一套默认的 CA 根证书

作为 JDK 一部分的 cacerts 密钥库旨在包含一组能够用于在各种安全协议的证书链中建立信任的根证书。但是,JDK 源代码中的 cacerts 密钥库至目前为止一直是空的。因此,在 JDK 构建中,默认情况下,关键安全组件(如 TLS)是不起作用的。要解决此问题,用户必须使用一组根证书配置和 cacerts 密钥库下的 CA 根证书。

Java 11 中包含了传输层安全性(TLS)1.3 规范(RFC 8446)的实现,替换了之前版本中包含的 TLS,包括 TLS 1.2,同时还改进了其他 TLS 功能,例如 OCSP 装订扩展(RFC 6066,RFC 6961),以及会话散列和扩展主密钥扩展(RFC 7627),在安全性和性能方面也做了很多提升(之前版本中使用的 KRB5 密码套件实现已从 Java 11 中删除,因为该算法已不再安全。同时注意,TLS 1.3 与以前的版本不直接兼容)。

HTTP Client 升级

Java 11 对 Java 9 中引入并在 Java 10 中进行了更新的 Http Client API 进行了标准化,在前两个版本中进行孵化的同时,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。

新版 Java 中,Http Client 的包名由 jdk.incubator.http 改为 java.net.http,该 API 通过 CompleteableFutures 提供非阻塞请求和响应语义,可以联合使用以触发相应的动作,并且 RX Flo w 的概念也在 Java 11 中得到了实现。现在,在用户层请求发布者和响应发布者与底层套接字之间追踪数据流更容易了。这降低了复杂性,并最大程度上提高了 HTTP/1 和 HTTP/2 之间的重用的可能性。

5.3 JDK 17

在 SpringBoot 升级到 3 之后,JDK 最低版本要求为 17。JDK17 更快,高吞吐量垃圾回收器比低延迟垃圾回收器更快。

在 JDK 14 中,switch 正式可以使用 -> 进行简化(类似 kotlin 的 when)。JDK 17 推出 switch 的模式匹配,解决 instanceof 的连续判断问题,但是目前还是 preview 阶段,不建议在正式环境中使用。

public class SwitchDemo {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int id = in.nextInt();
        String res = switch (id) {
            case 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 -> "<10";
            case 11 -> "it's 10";
            default -> "default value";
        };
        System.out.println("res = " + res);
    }
}

JDK 15 确定使用文本块(类似 kotlin 中的多行字符串)。通过使用这样的文本块,在书写 json/SQL 等语句会更加清晰易懂。

String demoString = """
    select id \
    from account \
    where id=#{id};\
    """;
// demoString = select id from account where id=#{id};

var languages = """
    [
        {
            "name": "kotlin",
            "type": "static"
        },
        {
            "name": "julia",
            "type": "dynamic"
        }
	]
    """;

JDK 16 中正式改进 instanceof,可以在判断中直接进行类型转换(类似 C# 中的 is),不再需要每次都创建一个局部变量并进行强制转换。这个变量的范围可以延伸到之后的 && 判断中。

if (person instanceof Student student) {
    student.say();
   // other student operations
} else if (person instanceof Teacher teacher) {
    teacher.say();
    // other teacher operations
}

/*
if (person instanceof Student) {
    Student student = (Student) person;
    student.say();
} else if (person instanceof Teacher) {
    Teacher teacher = (Teacher) person;
    teacher.say();
}
*/

JDK 16 正式引入 Record。Record 这一特性主要用在特定领域的类上;与枚举类型一样,Record 类型是一种受限形式的类型,主要用于存储、保存数据,并且没有其它额外自定义行为的场景下。其效果有些类似 Lombok 的 @Data 注解、Kotlin 中的 data class。Record 描述的这个“类”只用来存储数据。

public record Person(String name, int age) {
}

// 会自动重写 tostring()、equals() 等等方法
public record Person(String name, int age) {
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() {
        return this.name;
    }

    public int age() {
        return this.age;
    }
}

JDK 17 正式引入 sealed 关键字,解释为密封类。封闭类可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。这个特性由 Java 15 的预览版本晋升为正式版本。

这样在写 API 或者其他功能时可以直接限制类的层次。

// 添加sealed修饰符,permits后面跟上只能被继承的子类名称
public sealed class Person permits Teacher, Worker, Student{ } //人
 
// 子类可以被修饰为 final
final class Teacher extends Person { }//教师
 
// 子类可以被修饰为 non-sealed,此时 Worker类就成了普通类,谁都可以继承它
non-sealed class Worker extends Person { }  //工人
// 任何类都可以继承Worker
class AnyClass extends Worker{}
 
//子类可以被修饰为 sealed,同上
sealed class Student extends Person permits MiddleSchoolStudent,GraduateStudent{ } //学生
final class MiddleSchoolStudent extends Student { }
final class GraduateStudent extends Student { }

参考文章

  1. Java 学习+面试整理
  2. 轻松看懂 Java 字节码
  3. UTF-8 到底是什么意思
  4. 后端八股文整合
  5. 为什么这么多人不喜欢用 goto?
  6. Is Java “pass-by-reference” or “pass-by-value”?
  7. Why don’t Java’s +=, -=, *=, /= compound assignment operators require casting?
  8. Chapter 15. Expressions (oracle.com)
  9. Java 中的复制构造函数和工厂方法
  10. Java 序列化与反序列化
  11. JDK 动态代理和 CGLib 动态代理的对比
  12. Why String is immutable in Java?
  13. ArrayList 的扩容机制
  14. HashMap 的实现原理
  15. 为什么 HashMap 的加载因子是0.75?
  16. Java 中的 String 为什么要设计成不可变的?
  17. 为什么 HashTable 被弃用了
  18. HashTable 和 Vector 为什么逐渐被废弃
  19. Java CopyOnWriteArrayList 详解
  20. Java HashMap的死循环
  21. HashMap? ConcurrentHashMap?
  22. Java 中的 transient 关键字详解
  23. Java 基础八股文背诵版
  24. 10 道 Java 泛型面试题
  25. ConcurrentHashMap,分段锁,CAS
  26. Java 动态代理
  27. Java 锁与线程的那些事
  28. Java 并发编程:Synchronized 底层优化(偏向锁、轻量级锁)
  29. Java Supplier
  30. Java 9 新工具 jshell 使用指南
  31. Java 17 新特性:switch 的模式匹配(Preview)
  32. 追随 Kotlin/Scala,看 Java 12-15 的现代语言特性
  33. 记录类 - 廖雪峰的官方网站

文章作者: 陈鑫扬
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 陈鑫扬 !
评论
  目录