java基础

1. ==和equals的区别

  1. 对于基本数据类型,“==”比较的是值是否相等;对于引用数据类型,“==”比较的是内存地址是否相等
  2. equals本质上来说是使用了==作为其底层的实现,但是java中很多类都对equals做了重写,使其对值进行判断

重写equals可以通过判断以下几点实现:

  • 检查是否为同一个对象的引用,如果是直接返回true
  • 检查是否是同一类型,不是直接返回false
  • 将Object进行转型
  • 判断每个关键域是否相等

例子:

String类中对equals方法进行了重写,使其判断String类型的数据的值是否相等,相等则返回true

String中equals底层实现(jdk11):

  1. 首先判断两个对象的地址是否相等,如果相等直接返回true,否则则进行下一步判断
  2. 接着判断对象实例是否是String类,如果不是直接返回false
  3. 判断两者的字符编码是否相同

    String类中定义了 COMPACT_STRINGS 表示是否开启字符串压缩,如果关闭则都使用UTF16编码

  4. 根据字符编码不同分别调用不同的字符类中的equals方法判断

    有两个字符编码类:Latin1(ISO-8859-1)UTF16

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                              : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}

两个字符类中equals的实现:

/* StringLatin*/
public static boolean equals(byte[] value, byte[] other) {
    if (value.length == other.length) {
        for (int i = 0; i < value.length; i++) {
            if (value[i] != other[i]) {
                return false;
            }
        }
        return true;
    }
    return false;
}

/* StringUTF16 */
public static boolean equals(byte[] value, byte[] other) {
    if (value.length == other.length) {
        //UTF16类型的数据,一个字符会占据两个byte数组位,所以计算长度时需要将byte数组长度缩小2倍
        int len = value.length >> 1;
        for (int i = 0; i < len; i++) {
            if (getChar(value, i) != getChar(other, i)) {
                return false;
            }
        }
        return true;
    }
    return false;
}
  • 说明:

    当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

使用equals方法注意事项:

  1. Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals
  2. 更推荐使用java.util.Object.equals(JDK7引入的工具类)
public static boolean equals(Object a, Object b) {
        // 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。
        return (a == b) || (a != null && a.equals(b));
    }
  1. 所有整形包装类对象值的比较都必须使用equals方法
Integer x = 3;//将3自动装箱成Integer
Integer y = 3;
System.out.println(x == y);// true
Integer a = new Integer(3);
Integer b = new Integer(3);
int c = 3;
System.out.println(a == b);//false
System.out.println(a.equals(b));//true
System.out.println(a == x);//false
System.out.println(a == c);//true,将a自动拆箱成int再比较

2. 为什么重写equals时必须重写hashCode方法?

默认情况下,Object 的 hashcode 方法是本地方法,也就是用 C 或者 C++ 语言实现的,该方法直接返回对象的内存地址。

  1. hashcode 获取得到的是该对象在哈希表中的索引位置,是一个 int 类型的数据,称为哈希码,也叫散列码
  2. hashcode 只有在需要用到散列表(HashSet, Hashtable, HashMap)的情况下才需要,不然没有任何关系
  3. hashcode使用情况实例:

    当在hashset中加入新对象时,hashset 首先会先计算 hashcode 来判断对象加入的位置,同时将该 hashcode 与其他已经在 hashset 中的对象的 hashcode 做比较,如果 hashcode 不相同,则认为对象不相同。如果存在 hashcode 相同的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。

  • hashCode()与equals()的相关规定:
  1. 如果两个对象相等,则 hashcode 一定也是相同的
  2. 两个对象相等,对两个对象分别调用 equals 方法都返回true
  3. 两个对象有相同的hashcode 值,它们也不一定是相等
  4. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

因此,要判断两个对象的值是否相等,除了重写equals 方法,hashCode方法也必须被覆盖

重写 hashcode 的思路:
理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R一般取31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出时,信息就会丢失,因为与2相乘相当于左移一位。

一个数与31相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。

//例如一个类中有x,y,z三个整数类型的属性:
@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}

3. List,Set,Map三者的区别

  1. List是有序列表,其存放内容是可以重复的,其实现接口有:ArrayListLinkedListVector
  2. Set是无序的集合,其内部存放的内容不可以重复,实现接口有HashSetTreeSetLinkedHashSet
  3. Map存的是键值对(key-value),key值不允许有重复,value内容可以相同,但对应不同的key,实现接口有HashMapTreeMap,LinkedHashMap

4. ArrayList源码分析

  1. 在使用默认构造方法(无参构造)时,创建的是一个空数组,只有当第一次插入数据时,才会将数组容量初始化为10
//private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
  1. ArrayList的扩容机制:
 //扩容方法:
 private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity));
 }
    
 //扩容核心方法:  
 private int newCapacity(int minCapacity) {
 
        int oldCapacity = elementData.length;
        //右移一位相当于除以2,即新的容量为原来容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //比较新容量跟指定容量大小
        if (newCapacity - minCapacity <= 0) {
            //如果minCapacity比较大
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                //返回默认容量和minCapacity中较大的
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        //如果newCapacity比较大
        //如果小于MAX_ARRRY_SIZE,则返回newCapacity,否则返回Integer最大值
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE)
            ? Integer.MAX_VALUE
            : MAX_ARRAY_SIZE;
    }
  1. ArrayList复制的方式
    ArrayList源码中用到的复制方法有两个:System.arraycopyArrays.copyOf

两者的区别:

  • Arrays.copyOf本质上是调用了System.arraycopy方法
  • arraycopy需要传入目标数组,将复制后的数据放到目标数组中,而copyOf是在方法内部创建一个数组,将内容复制到这个数组然后返回该数组

copyOf的一个方法的源码:

 public static byte[] copyOf(byte[] original, int newLength) {
        byte[] copy = new byte[newLength];
        //本质上调用了arraycopy
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

arraycopy的源码:

/**
  * @param      src      源数组
  * @param      srcPos   要复制的源数组的起始位置
  * @param      dest     目标数组
  * @param      destPos  目标数组起始位置
  * @param      length   复制的长度.
  */
public static native void arraycopy(Object src,int srcPos,Object dest,int destPos,int length);

ArrayList中add方法通过arraycopy方法复制自身数组,并将后面数组右移实现在指定位置插入内容

public void add(int index, E element) {
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
        //复制自身数组,将index位置的数据后移
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        //index位置放入数据
        elementData[index] = element;
        size = s + 1;
    }
  1. 序列化

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

transient Object[] elementData; // non-private to simplify nested class access

ArrayList实现了writeObject()readObject来控制只序列化数组中有元素填充的那部分内容。

//序列化
 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);
    }
}

序列化和反序列化需要使用ObjectOutputStream中的writeObjectObjectInputStream中的readObject方法,这两种方法在传入对象的内存存在writeObjectreadObject时,会反射调用该对象的内部序列化方法。

5. LinkList源码分析

LinkedList是一个实现了List接口和Deque接口的双向链表。
LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:

List list=Collections.synchronizedList(new LinkedList(...));

LinkList中获取指定位置节点的方法

/**
 * Returns the (non-null) Node at the specified element index.
 */
Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {//折半查找
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

LinkList将集合插入到链表尾部:

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

addAll(int Index,Collection c):将集合从指定位置开始插入具体实现:

public boolean addAll(int index, Collection<? extends E> c) {
    //1. 检查index是否在size范围内
    checkPositionIndex(index);

    //2. 把集合的数据存到对象数组中
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    Node<E> pred, succ;
    //3. 得到插入位置的前驱节点和后继节点
    //如果插入位置是尾部,前驱节点是last,后继节点是null
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        //否则,调用node()方法得到后继节点,再得到前驱节点
        succ = node(index);
        pred = succ.prev;
    }

    //4. 遍历数组将数据插入
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //创建新节点,同时指定前驱节点
        Node<E> newNode = new Node<>(pred, e, null);
        //如果插入位置是首节点
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;//双向链表,前驱节点的指针指向当前节点
        pred = newNode;//节点后移
    }

    //如果插入位置是在尾部,重置last节点
    if (succ == null) {
        last = pred;
    } else {
        //否则,将插入链表与原链表的后半部分连接起来
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;
    modCount++;
    return true;
}

由以上可以总结出addAll方法的步骤:

  1. 检查index范围是否在size范围内
  2. toArray方法把集合的数据存到对象数组中
  3. 得到插入位置的前驱和后继节点
  4. 遍历数据插入
  5. 将插入后的链表与后半部分链接

LinkedList获取头节点数据的方法:

public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
public E element() {
    return getFirst();
}
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}
public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

区别getFirstelement方法在链表为空时会抛出异常NoSuchElementException,而peekpeekFirst会直接返回null。

6. String,StringBuffer和StringBuilder的区别:

  • String被声明为final,不可以被继承,在java8中,String使用char数组存储数据,jave9之后使用byte数组存储字符串,同时使用coder标识使用了哪种编码。
  • StringBufferStringBuilder可变
  • String不可变,线程安全;
  • StringBuilder线程不安全,但性能高,字符串相加在编译后会使用该类优化代码实现拼接
  • StringBuffer线程安全,性能低,内部使用synchronized进行同步。

    字符串如果使用“+”拼接,每使用一次,就会产生一个对象,StringBuffer可以解决此类问题产生的性能问题。默认初始化的数组长度是16,超过时采用原长x2+2扩容。

单线程下,StringBuilder 和 StringBuffer 的性能其实相差不大,原因是 synchronized 引入了偏向锁。

7. String两种赋值方式比较:

String name1 = "xiaoming";
Stirng name2 = new String("xiaoming");

name1 == name2;//false

方式1会自动地将字符串放到字符串常量池之中,同时在栈内存会有指向堆内存的字符串常量池的指针。

方式2首先会寻找常量池中是否存在该常量,若存在则只在堆内存创建一个对象;否则,会首先在堆内存创建一个对象,然后在常量池创建一个字符串常量,name指向堆内存对象,而堆内存存储指向常量池的地址

8. String创建对象五种情况分析:

  • 变量的值只有在运行期才会被确定
  • 如果在编译期可以确定,那么使用已有的对象,否则创建新的对象

情况1

String a = "a";//常量池编译器确定
Strnig a1 = a+"1";//运行期确定(a1)
String a2 = "a1";//常量池编译器确定

a1 == a2;//false

情况2

final String b = "b";//final修饰的变量为静态,编译器确定
String b1 = b+"1";//编译器可以读取静态变量,编译器确定,放入常量池
String b2 = "b1";//获取常量池的值,编译器确定

b1 == b2;//true

情况3

String c = getString();//通过方法取值,只能在运行期确定
String c1 = c+1;//运行期确定
String c2 = "c1";//编译期确定

c1==c2;//false

private static String getString(){
    return "c";
}

情况4

final String d = getString();//虽然是个final常量,但是方法还是在运行期才确定
String d1 = d+1;
String d2 = "d1";

d1==d2;//false

private static String getString(){
    return "d";
}

情况5

String a = "a";
String b = "b";
String c = a+b+1;//运行时先产生a+b对象,再产生a+b+1对象
String d = "a"+"b"+1;//常量相加,只产生一个对象

c==d;//false

9. 接口与抽象类区别

抽象类

  1. 抽象类和抽象方法都使用abstract修饰
  2. 一个类中如果包含抽象方法,那么这个类必须声明为抽象类。
  3. 抽象类不能被实例化,需要继承抽象类才能实例化其子类。
  4. 抽象类不能使用final修饰
  5. 抽象类可以有实现方法和属性、构造方法

接口

  1. 使用interface关键字
  2. 接口中可以定义常量,抽象方法,jdk8之后可以有默认实现方法和静态方法
  3. 可以继承多个接口
  4. 接口中的方法和字段默认是public,不允许定义为private和protect
  5. 接口不能有构造方法
  6. 接口的字段默认是 staticfinal

10. 重写和重载的区别

重写存在于继承体系之中,指子类实现了一个与父类在方法声明上完全相同的方法。

为了满足里式替换原则,重写必须满足以下三个原则:

  • 子类的访问权限不能低于父类的访问权限
  • 子类方法返回类型必须是父类方法返回类型或其子类
  • 子类方法抛出的异常必须是父类方法抛出的异常或其子类

里式替换原则:里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。

在调用一个方法时,先从本类中查找是否有相应的方法,如果没有就到父类中查找。如果没有就对参数进行转型,然后查看本类是否有相应的方法,没有就在父类中找,顺序如下:

  • this.func(this)
  • super.func(this)
  • this.func(super)
  • super.func(super)

重载存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同,与返回值无关

11. 浅克隆和深克隆

cloneObjectprotected方法,因此其他类不能直接去调用该类实例的clone方法,需要对clone进行重写,重写时需要继承Cloneable标记接口,否则会报CloneNotSupportedException

浅克隆:当对象的属性中有引用变量时,实际上克隆后的对象跟原有对象所指向的是同一地址,如果此时修改引用变量的值,会使两个对象的引用变量都发生改变。

深克隆:将引用对象也做一份拷贝

public class Sheep implements Cloneable{
    private String name;
    private Date birthday;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Object obj = super.clone();
        //深克隆添加如下代码:
        Sheep sheep = (Sheep)obj;
        sheep.birthday = (Date) this.birthday.clone();//把属性也进行克隆
        return sheep;
    }

    public Sheep(String name, Date birthday) {
        this.name = name;
        this.birthday = birthday;
    }
      
      //省略getter,setter和toString方法
}


//客户端操作
public class Client {
    public static void main(String[] args) throws CloneNotSupportedException {
        Date date = new Date(System.currentTimeMillis());
        Sheep sheep1 = new Sheep("多莉",date);
        Sheep sheep2 = (Sheep) sheep1.clone();

        date.setTime(23490738574947548L);//改变时间值,如果是浅克隆,两个都会改变

        System.out.println(sheep1);
        System.out.println(sheep2);
        System.out.println(sheep1 == sheep2);
    }
}

clone替代方案

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

public class CloneConstructorExample {

    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }
}

12. 类加载时的初始化顺序

  1. 静态变量和静态代码块(两者谁先取决于代码顺序)
  2. 实例变量
  3. 普通语句快(构造代码块)
  4. 构造函数的初始化

如果存在继承的情况,初始化顺序如下:

  • 父类(静态变量,静态代码块)
  • 子类(静态变量,静态代码块)
  • 父类(实例变量,普通语句块)
  • 父类(构造函数)
  • 子类(实例变量,普通语句块)
  • 子类(构造函数)

13. 构造器Constructor是否可以被重写?

父类的私有属性和构造方法不能被继承,所以Constructor不能被重写,但是可以重载。

14. 对象实例与对象引用有什么不同?

对象实例在堆内存中,一个对象引用指向对象实例(对象引用在栈内存中)。一个对象引用可以指向0个或1个对象实例;一个对象实例可以有n个引用指向它。

15. 为什么java只有值传递?

对于基本数据类型,java参数传递的是值的复制;对于引用数据类型,java参数传递的是指向地址的指针的拷贝。

引用传递一般是对于对象型变量而言,传递的是该对象地址的一个副本,并不是原对象本身。所以对引用对象进行操作会同时改变原有对象。

16. 程序、进程、线程的区别

程序是含有指令和数据的文件,被存在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。

线程与进程相似。但是线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与线程不同的的是同类的多个线程共享同一块内存空间和一组系统资源(共享进程的堆和方法区资源),但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小。

线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

在 java 中:

java运行时内存区域

从上图可以看出,一个进程中可以有多个线程,多个线程共享进程的方法区资源(JDK1.8之后使用元空间),但是每个线程有自己的程序计数器,虚拟机栈和本地方法栈

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

线程是进程划分成更小的运行单位。线程和进程最大的不同在于基本上各个进程都是独立的,而线程则不一定。同一进程中的不同线程极有可能相互影响。线程执行开销小,但不利于资源的管理和保护;进程则相反。

17. java线程有哪些基本状态

状态名称 说明
NEW 初始状态,线程被构建,但是还没有调用start方法
RUNNABLE 运行状态,java线程将操作系统中的就绪和运行两种状态笼统的称作“运作中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断)
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕

18. java中的异常处理

java异常类层次结构图

在java中,所有的异常都有一个共同的祖先java.lang包中的的Throwable类。它有两个重要的子类:Exception(异常)Error(错误),二者都是java处理异常的重要子类。

ERROR

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(VirtualMachineError),当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通过Error的子类描述。

EXCEPTION

Exception(异常):是程序本身可以处理的异常Exception类有一个重要的子类RuntimeException。该异常由Java虚拟机抛出。

  • ArrithmeticException:算术运算异常,一个整数除以0时,会抛出该异常。
  • MissingResourceException
  • ClassNotFoundException
  • NullPointerException:要访问的变量没有引用任何对象时抛出该异常。
  • IllegalArgumentException:非法参数异常
  • ArrayIndexOutOfBoundsException:下标越界异常
  • UnkownTypeException

异常和错误的区别:异常能被程序本身处理,错误无法处理

Throwable类常用方法

  • public String getMessage():返回异常发生时的详细信息
  • public string toString():返回异常发生时的简要描述
  • public string getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
  • public void printStackTrace():在控制台上打印Throwable对象封装的异常信息

异常处理总结

  • try块: 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。

  • catch块:用于处理try捕获到的异常

  • finally块:无论是否捕获或处理异常,finally中的语句都会被执行。

    当try和finaly语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。

  • throw:用于在代码中明确的排除一个异常

  • throws:用在方法声明上标明函数可能抛出的各种异常。

finally块不会被执行的四种情况:

  1. 在finally语句块第一行发生了异常。因为在其他行,finally块还是会得到执行
  2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数;若该语句在异常语句之后,finally会执行
  3. 程序所在的线程死亡。
  4. 关闭CPU。

19. Java中的IO流分为几种?BIO,NIO,AIO的区别?

JAVA中IO流分类

  • 按照流的流向,分为输入流输出流
  • 按照操作单元划分,分为字节流字符流
  • 按照流的角色划分为节点流处理流

按照操作方式分类的结构图:

按照操作方式分类结构图

按照操作对象分类结构图:

按照操作对象分类结构图

BIO,NIO,AIO区别

  • BIO(Blocking I/O):同步阻塞I/O,数据的读取和写入都必须阻塞在一个进程内等待完成。这种方式在连接数不是特别高(小于单机1000)的情况下,这种模型比较不错,可以让每一个连接都专注于自己的I/O,并且编程模型简单,也不用过多的考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。因此,需要一种更高效的I/O处理模型来应对更高的并发量。
  • NIO(New I/O):NIO是一种同步非阻塞的I/O模型,它是支持面向缓冲的,基于通道的I/O操作方法。但是其I/O行为还是同步的,对于NIO来说,业务线程是在IO操作准备好时,才得到通知,接着就由这个线程自行完成IO操作,IO操作本身是同步的。
  • AIO(Asynchronous I/O):AIO也就是NIO2,是异步非阻塞模式。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

20. 获取键盘输入常用的两种方法:

  1. 通过Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
  1. 通过BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();

21. BigDecimal用处

《阿里巴巴Java开发手册》中提到:浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。具体原理和浮点数的编码方式有关

float a = 1.0f-0.9f;
float b = 0.9f-0.8f;
System.out.println(a);//0.100000024
System.out.println(b);//0.099999964
System.out.println(a ==b);//false

上面代码会出现false的原因是因为float数据存在精度丢失的情况。二进制的小数无法精确的表达十进制小数,所以计算法在计算十进制小数的过程中要先转化为二进制进行计算,这个过程就会出现精度丢失的情况

如何解决这个问题?一种常用的方法是:使用BigDecimal来定义浮点数的值,再进行浮点数的运算操作

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.substract(b);//0.1
BigDecimal y = b.substract(c);//0.1
System.out.println(x.equals(y));//true;

BigDecimal的大小比较

a.compareTo(b):返回-1表示小于,返回0表示等于,返回1表示大于

BigDecimal保留几位小数

通过setScale方法设置保留几位小数以及保留规则。

BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
System.out.println(n);// 1.255

BigDecimal使用注意事项

《阿里巴巴Java开发手册》提到:为了防止精度损失,禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象。该方法存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。优先推荐使用参数为String的构造方法,或者 BigDecimal 的valueOf方法,此方法内存其实执行了 Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。

22. Arrays.asList()使用指南

JDK中对于这个方法的源码:

/**
 *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。
 */ 
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

《阿里巴巴Java开发手册》对其的描述:

《阿里巴巴Java 开发手册》对其的描述

使用时注意事项总结

  1. 传递的数组必须是对象数组,而不是基本类型
int[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//数组地址值
System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0);
System.out.println(array[0]);//1

当传入一个原生数据类型数组时,Arrays.asList()真正得到的参数就不是数组中的元素,而是数组对象本身!此时List的唯一元素就是这个数组,这也就解释了上面的代码。

使用包装类就可以解决这个问题

Integer[] myArray = { 1, 2, 3 };
  1. 使用集合的修改方法:add()、remove()、clear()会抛出异常。
List myList = Arrays.asList(1, 2, 3);
myList.add(4);//运行时报错:UnsupportedOperationException
myList.remove(1);//运行时报错:UnsupportedOperationException
myList.clear();//运行时报错:UnsupportedOperationException

Arrays.asList()方法返回的并不是java.util.ArrayList,而是java.util.Arrays的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。

List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList

ArrayList继承了AbstractList,而在AbstractList中,这几个方法的实现就是抛出UnsupportedOperationException异常,该ArrayList并没有对这些方法进行重写。

如何正确的将数组转化为ArrayList?

  1. 自己动手实现
static <T> List<T> arrayToList(final T[] array) {
    final List<T> l = new ArrayList<T>(array.length);
    
    for(final T s : array){
        l.add(s);
    }
    return l;
}
  1. 最简便的方法(推荐)
List list = new ArrayList<>(Arrays.asList("a","b","c"));
  1. 使用java8的Stream(推荐)
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());

23. Collection.toArray()方法的使用以及如何反转数组

该方法是一个泛型方法:<T> T[] toArray(T[] a); 如果toArray方法中没有传递任何参数的话返回的是Object类型数组。

String [] s= new String[]{
    "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
s=list.toArray(new String[0]);//没有指定类型的话会报错

由于JVM优化,new String[0]作为Collection.toArray()方法的参数现在使用更好,new String[0]就是起一个模板的作用,指定了返回数组的类型,0是为了节省空间,因为它只是为了说明返回的类型。

24. 为什么不要在foreach循环里进行元素的remove/add操作

《阿里巴巴Java开发手册》

如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。因为如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身remove/add方法,迭代器都将抛出一个ConcurrentModificationException,这就是单线程状态下产生的fail-fast机制。

fail-fast,即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException( 当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。需要注意,即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。

源码解析

对上述代码进行反编译后,得到

List a = new ArrayList();
a.add("1");
a.add("2");
Iterator i$ = a.iterator();
do {
    if (!i$.hasNext())
        break;
    String temp = (String) i$.next();
    if ("2".equals(temp))
        a.remove(temp);
} while (true);

首先需要注意的几个点是:

  • foreach循环内部其实使用的是Iterator迭代器
  • 代码首先判断是否hasNext,然后再去调用next
  • 这里的remove还是list的remove方法。

list中的remove源码

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    //寻找目标位置
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);//删除的关键方法
    return true;
}

fastRemove源码

private void fastRemove(Object[] es, int i) {
    modCount++;//此处都修改次数计数+1,是关键
    final int newSize;
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}

add方法源码

public boolean add(E e) {
    modCount++;//同样有该操作
    add(e, elementData, size);
    return true;
}

iterator源码

public Iterator<E> iterator() {
    return new Itr();
}
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    // prevent creating a synthetic constructor
    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        //此方法是抛出ConcurrentModificationException异常的根本原因,具体实现看该类最后
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i < size) {
            final Object[] es = elementData;
            if (i >= es.length)
                throw new ConcurrentModificationException();
            for (; i < size && modCount == expectedModCount; i++)
                action.accept(elementAt(es, i));
            // update once at end to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

通过源码可以看到,每一次对集合进行添加或者删除,都会对数组标记modCount++,在iterator中进行迭代时,在进行next判断时,会用iterator中的expectedModCountmodCount进行比较,如果不一样就会抛出ConcurrentModificationException。这也就是fail-fast快速失败机制。

在上面的例子中:
第一个例子执行完第一次循环后,mod = 3 expectedModCount =2 cursor = 1 size = 1,所以程序在执行hasNext()的时候判断会返回false,所以程序不会报错。

第二个例子执行完第二次循环后,mod = 3 expectdModCount = 2 cursor = 2 size = 1 此时cursor != size,程序认定还有元素,继续执行循环,调用next方法但是此时mod != expectedModCount所以此时会报错。

只有调用iterator中的remove方法,才会对expectedModCount进行更新,此时才不会报错。

25. Servlet接口中有哪些方法以及Servlet的生命周期

Servlet中定义了5个方法,其中前三个方法与Servlet生命周期有关:

  • void init(ServletConfig config) throws ServletException
  • void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException
  • void destroy()
  • java.lang.String getServletInfo()
  • ServletConfig getServletConfig()

生命周期:Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其init()方法进行Servlet的初始化;请求到达时调用Servlet的service()方法,service()方法会根据需要调用与请求对应的doGetdoPost等方法;当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的destroy()方法。**init方法和destroy方法只会执行一次,service方法客户端每次请求Servlet都会执行。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。(单例**)

26. Servlet线程安全问题

Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。解决的办法是尽量不要在Servlet类中定义name属性(成员变量),而是要把name变量分别定义在doGet()doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程的等待,不是很科学的办法。

注意:多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类型的。

27. HashMap源码分析

JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。

HashMap类中的属性

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    //序列号
    private static final long serialVersionUID = 362498820763181265L;
    //默认初始化容量,16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //允许的最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //当桶(bucket)上的结点数大于这个值时会转化成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    //当桶(bucket)上的结点数小于这个值时转化成链表
    static final int UNTREEIFY_THRESHOLD = 6;
    //桶中结构转换成红黑树对应table的最大大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    //存储元素的数组,总是2的幂次倍
    transient Node<K,V>[] table;
    //存放具体元素的集
    transient Set<Map.Entry<K,V>> entrySet;
    //存放元素的个数
    transient int size;
    //每次扩容和更改map的计数器
    transient int modCount;
    //临界值,当实际大小(容量*加载因子)超过临界值时,会进行扩容
    int threshold;
    //加载因子
    final float loadFactor;
  • loadFactor:加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

  • thresholdthreshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。

HashMap获得hash值方法

HashMap通过keyhashCode经过扰动函数处理过后得到hash值,然后通过(n - 1) & hash判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法 换句话说使用扰动函数之后可以减少碰撞。

static final int hash(Object key) {
    int h;
    // ^:按位异或
    // >>>:无符号右移,忽略符号位,空位以0补齐
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

内部节点类分析

Node节点

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;//哈希值,存放元素到hashmap中时用来与其他元素hash值比较
    final K key;//键
    V value;//值
    Node<K,V> next;//下一个结点

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    //重写hashcode方法
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    //重写equals方法
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

树结点类

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;//判断颜色
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

    //返回根结点
    final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }
    }
    //后面的省略,在下面分析...

常用方法分析

构造方法

HashMap中有四个构造方法:

//默认构造方法
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//包含一个Map的构造方法
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

//指定初始容量的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
    
//指定初始容量和加载因子的构造函数
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);//保证容量会是2的幂次方大小
}

putMapEntries方法

用于将一个Map集合放入HashMap中

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        //如果此时table还没有初始化
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;//设置初始化的容量为当前map大小除以加载因子再加1
            //判断t有没有超过允许的最大大小
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            //如果t>阈值,初始化阈值
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        //如果table已经初始化,而且m的个数大于阈值,则进行扩容
        else if (s > threshold)
            resize();
        //将m中的所有元素放到hashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

Put方法

HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。

对putVal方法添加元素的分析如下:

①如果定位到的数组位置没有元素就直接插入。

②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
插入数据流程图

时间复杂度:理想情况下,不会出现 hash 冲突,此时时间复杂度是 O(1);如果桶里有元素,并且元素个数小于8,则需要进行遍历链表,此时时间复杂度为 O(n);如果桶里有元素并且个数大于8,此时转换为红黑树,时间复杂度为 O(logn);因此最理想状态是 O(1),最差是O(n).

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * @param onlyIfAbsent 表示是否对旧值进行替换,为true时表示不替换
 * @param evict 表示table是否处于创建状态,false表示处于
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
            boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //table为初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //此时该位置已经有元素
    else {
        Node<K,V> e; K k;
        //比较当前桶的第一个元素的hash值相等,key也相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;//记录此时原有旧元素,后面会将新的值放入
        //如果hash值不相等且为红黑树结点
        else if (p instanceof TreeNode)
            //放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //为链表结点
        else {
            //循环在链表末端插入结点
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果结点数量达到阈值,转化成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //判断链表结点中的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    //相等就跳出循环
                    break;
                //用于遍历链表
                p = e;
            }
        }
        //表示前面在桶中找到key,hash值与插入元素相等的结点
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            //根据所传参数决定是否替换旧值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);//插入后回调
            return oldValue;
        }
    }
    ++modCount;//修改此处+1
    //如果大小大于阈值就扩容
    if (++size > threshold)
        resize();
    //回调(主要是在子类LinkedHashMap中使用)
    afterNodeInsertion(evict);
    return null;
}

get方法

最优情况,hash不碰撞,O(1),典型情况,近似是O(1),因为几乎没有碰撞,最坏情况,O(N),也就是所有的hash都一样,那么退化为线性查找。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                //在树中寻找
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do { //在链表中循环查找
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

resize扩容方法

进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        //超过最大值就不再扩容了,随你去碰撞
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //新容量为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //没有初始化数组
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //设置新的resize上限(阈值)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                 (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        //把每个bucket都移到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //当前位置只有一个结点
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //如果是红黑树,则转到红黑树中进行迁移
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    //当前结点是一个链表
                    Node<K,V> loHead = null, loTail = null;//散列后是原索引的链表
                    Node<K,V> hiHead = null, hiTail = null;//散列后是新索引的链表
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //与原数组容量与运算,相当于在最高位再取一个位
                        if ((e.hash & oldCap) == 0) {
                            //如果是0,不变,使用原索引
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            //等于1就重新散列
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //新的位置为原索引+旧容量(原因是在更高一位添加一个1相当于原有加上2的n次方(n表示位数),正好相当于原数组容量+当前位置散列(原数组容量本身就是2的n次方)
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

以下对重新散列做一些说明

假如现在容量为初始容量16,再假如5,21,37,53的hash自己(二进制),所以在oldTab中的存储位置就都是hash & (16 - 1)【16-1就是二进制1111,就是取最后四位】,

  • 5 :00000101
  • 21:00010101
  • 37:00100101
  • 53:00110101

四个数与(16-1)相与后都是0101,即原始链为:5--->21--->37--->53---->null

此时进入代码中do-while循环,对链表节点进行遍历,判断是留下还是去新的链表:

  • lo就是扩容后仍然在原地的元素链表
  • hi就是扩容后下标为原位置+原数组容量的元素链表,从而不需要重新计算hash。

因为扩容后计算存储位置就是hash & (32 - 1)【取后5位】,但是并不需要再计算一次位置,
此处只需要判断左边新增的那一位(右数第5位)是否为1即可判断此节点是留在原地lo还是移动去高位hi

(e.hash & oldCap) == 0 (oldCap是16也就是10000,相与即取新的那一位)

  • 5 :00000101——————》0留在原地 lo链表
  • 21:00010101——————》1移向高位 hi链表
  • 37:00100101——————》0留在原地 lo链表
  • 53:00110101——————》1移向高位 hi链表

为什么为0就放在原位置,为1就要放到原位置+原数组容量位置呢

因为上面进行resize的时候,是将数组容量扩大了一倍,原计算位置取模的时候是通过length-1,那么现在与oldCap相与后,如果右边第5位是1,那就是增加了一个原数组的长度(因为原取模的时候是取4位),所以如果右边第5位要是1的话,那新的位置就是原位置+原数组容量。

所以在1.8的情况下,resize 是不需要对原数组链表中的所有节点都进行再次hash,移动之后的节点也的顺序也不会改变,而且在一定程度上也避免了1.7中死锁的发生。

28. ArrayList和LinkedList区别?

  1. ArrayListLinkedList都是不同步的,也就是都是线程不安全的
  2. ArrayList底层使用的是Object数组,LinkedList底层使用的是双向链表的数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环)
  3. 插入和删除受元素位置的影响:
  • ArrayList采用数组存储,所以插入和删除的时间复杂度受元素位置的影响。比如执行add(E e),ArrayList会默认将指定元素追加到列表的末尾,这时时间复杂度为O(1);但是如果要在指定位置i插入和删除元素的话(add(int index,E element)),时间复杂度为O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
  • LinkedList采用链表存储,所以插入,删除元素的时间复杂度不受元素位置的影响,都是近似O(1),而数组近似O(n)
  1. LinkedList不支持高效随机访问元素,而ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象。
  2. ArrayList的空间浪费主要体现在 list 列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

RandomAccess接口

ArrayList继承了一个RandomAccess接口,这个接口在源码中并没有什么定义(相当于一个标识接口),用于标识这个接口的类具有随机访问的功能。
Collections工具类(提供大量针对Collection的操作)的binarySearch()中,它要判断传入的 list 是否RamdomAccess的实例,如果是,调用indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法。

list的遍历选择:

  • 实现了RandomAccess接口的list(表明支持快速随机访问),优先使用普通for循环,其次使用foreach
  • 未实现RandomAccess接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大size的数据,千万不要使用普通for循环。

29. HashMap和Hashtable的区别

  1. 线程是否安全HashMap是非线程安全的,HashTable是线程安全;HashTable内部的方法基本都经过synchronized修饰。(要保证线程安全使用ConcurrentHashMap

  2. 效率:因为线程安全问题,HashMap要比HashTable效率高一点。

    HashTable基本被淘汰,不要在代码中使用它

  3. 对Null key和Null value的支持HashMap中,null可以作为键,这样的键只能有一个,可以有一个或多个键所对应的值为null。但是在HashTableput键值只要有一个null,直接抛出NullPointException

  4. 初始容量大小和每次扩充容量大小的不同

  • 创建时如果不指定容量初始值,Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1HashMap默认初始化大小为16,之后每次扩容,容量变为原来的2倍。
  • 创建时如果给定了容量初始值,HashTable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小:
static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
  1. 底层数据结构: JDK1.8 以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable没有这样的机制。

30. HashSet(HashMap和HashSet区别)

HashSet是基于HashMap实现的,在HashSet源码中,除了clone(),writeObject(),readObject()HashSet自己实现的以外,其他的基本是调用HashMap中的方法。

HashSet检查重复的方式

HashSetadd方法实际上是调用了HashMapput方法,将HashSet的值作为Map中的键存储,利用了HasnMap键不能重复的特点。而在HashMap中,对键的重复检测,首先会计算hashcode值是否相等,如果不相等就加入;如果相等则使用equals()方法检查hashcode相等的对象是否真的相同。

//作为统一的value,是连接HashMap的桥梁
private static final Object PRESENT = new Object();

//添加方法
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

HashSet集合中并没有提供get()方法,当需要获取HashSet中某个元素时,只能通过遍历集合的方式进行equals()来比较实现:

for(String str:hashSet){
    if("xiaoming".equals(str)){
        System.out.println("匹配到了"+str);
    }
}

31. HashMap 为什么线程不安全?

主要有两个场景:

put的时候导致的多线程数据不一致。

这个问题比较好想象,比如有两个线程 A 和 B,首先 A 希望插入一个 key-value 对到 HashMap 中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程 B 被调度得以执行,和线程 A 一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程 B 成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程 B 插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

多线程下 Rehash 导致的死循环问题

主要原因在于并发下的Rehash会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,在 1.8 的情况下,resize 是不需要对原数组链表中的所有节点都进行再次 hash,移动之后的节点也的顺序也不会改变,而且在一定程度上也避免了1.7中死锁的发生

并发环境下推荐使用 ConcurrentHashMap

32. ConcurrentHashMap

get 方法

源码如下:

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode()); //计算hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素,底层调用 unsafe 类的 CAS 操作
        if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
        //eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
        //eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

ConcurrentHashMap 的读操作并不需要加锁,但是通过使用 volatile 关键字修饰 Node 节点的 val 和指针 next,在多线程环境下线程A修改因为hash冲突修改结点的 val 或者新增节点的时候是对线程 B 可见的。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    //可以看到这些都用了volatile修饰
    volatile V val;
    volatile Node<K,V> next;
    
    //...
}

ConcurrentHashMap 在其数组上也用了 volatile 修饰,其目的是为了使得 Node 数组在扩容的时候对其他线程具有可见性

/**
 * 垃圾箱数组。 第一次插入时延迟进行初始化。
 * 大小始终是2的幂。 由迭代器直接访问。
 */
transient volatile Node<K,V>[] table;

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMapHashtable区别主要体现在实现线程安全方式上不同。

  • 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树Hashtable和 JDK1.8 之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式
  1. 在JDK1.7的时候,ConcurrentHashMap采用的是分段锁的形式对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中的一部分数据,多线程访问容器里的不同数据段的数据,就不会存在锁竞争,提高并发效率。JDK1.8之后,摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronizedCAS(乐观锁)来操作。
  2. HashTable采用同一把锁的形式,使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。

两者对比图

HashTable:

HashTable安全锁

1.7的ConcurrentHashMap:

1.7ConcurrentHashMap

一个 ConcurrentHashMap 里包含两个静态内部类 HashEntrySegment 。HashEntry用来封装散列到映射表中的键值对,Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

在 HashEntry 类中,keyhashnext 域都被声明为 final 型,value 域被声明为 volatile 型。

static final class HashEntry<K,V> {
       final K key;                      // 声明 key 为 final 型
       final int hash;                   // 声明 hash 值为 final 型
       volatile V value;                 // 声明 value 为 volatile 型
       final HashEntry<K,V> next;        // 声明 next 为 final 型
  
       HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
           this.key = key;
           this.hash = hash;
           this.next = next;
           this.value = value;
       }
}

由于 HashEntry 的 next 域为 final 型,所以新节点只能在链表的表头处插入。 下图是在一个空桶中依次插入 A,B,C 三个 HashEntry 对象后的结构图:

ConcurrentHashMap1.7插入

由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反。

1.8的ConcurrentHashMap

1.8ConcurrentHashMap

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))

synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

ConcurrentHashMap有什么缺陷吗?

ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。

33. Comparable和Comparator区别

  • comparable接口实际上是出自java.lang包,它有一个compareTo(Object obj)方法用来排序
  • comparator接口实际上是出自java.util包,它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort()

Comparator定制排序:

        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        arrayList.add(-1);
        arrayList.add(3);
        arrayList.add(3);
        arrayList.add(-5);
        arrayList.add(7);
        arrayList.add(4);
        arrayList.add(-9);
        arrayList.add(-7);
        System.out.println("原始数组:");
        System.out.println(arrayList);
        // void reverse(List list):反转
        Collections.reverse(arrayList);
        System.out.println("Collections.reverse(arrayList):");
        System.out.println(arrayList);

        // void sort(List list),按自然排序的升序排序
        Collections.sort(arrayList);
        System.out.println("Collections.sort(arrayList):");
        System.out.println(arrayList);
        // 定制排序的用法
        Collections.sort(arrayList, new Comparator<Integer>() {

            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        System.out.println("定制排序后:");
        System.out.println(arrayList);

输出如下:

原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[7, 4, 3, 3, -1, -5, -7, -9]

对于普通的类,当类对象里有多个变量时,无法比较,此时需要使用让类继承ComparableComparator接口进行指定

//Comparable是泛型,可以指定类
class Cat implements Comparable<Cat>{
    
    private String name;
    private int age;

    //省略构造方法

    //实现接口中的方法
    public int compareTo(Cat o){
        //该方法用于比较此对象与指定对象的顺序,如果该对象小于,等于,大于指定对象,则分别返回负整数,零或正整数
        return this.age-o.age;
    }
}

Cat[] cats = {new Cat("小a",2),new Cat("小b",1),new Cat("小c",6)};
//此时可以进行比较
Arrays.sort(cats);

对于已经创建好的类,按照oo原则,对修改关闭,对扩展开放,所以可以使用Comparator接口重新定义一个新类来实现比较(该接口同样是泛型),该接口内有compare方法

public class CatComparator implements Comparator<Cat>{
    public int compare(Cat o1,Cat o2){
        return o1.getAge()-o2.getAge();
    }
}
//第一个参数是要比较的类,第二个是使用的比较器
Arrays.sort(cats,new CatComparator());

34. 容器中用到的设计模式

迭代器模式

Collection继承了Iterable接口,其中iterator()方法能够产生一个Iterator对象,通过这个对象就可以迭代遍历Collection中的元素。

适配器模式

java.util.Arrays.asList()可以把数组类型转化为List类型。

关于其内容见22题

35. Vector相关分析

同步

Vector的实现与ArrayList相似,但是使用了synchronized进行同步。

public synchronized boolean add(E e) {
    modCount++;
    add(e, elementData, elementCount);
    return true;
}

public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);

    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                        numMoved);
    elementData[--elementCount] = null; // Let gc do its work

    return oldValue;
}

public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

扩容

Vector可以通过构造函数传入capacityIncrement参数指定每次扩容时容量的增长数。默认情况下每次扩容会扩容为原来的两倍。

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                            initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement capacityIncrement;
}
private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity <= 0) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}

与ArrayList区别

  1. ArrayList是线程不安全的,而Vector是线程安全的,因此开销比ArrayList大,访问速度较慢。最好使用ArrayList,因为同步操作完全可以由程序员自己来控制。
  2. Vector每次扩容是默认容量为原来2倍(可以自行指定增长数量),ArrayList为1.5倍。

替代方案

可以使用Collections.synchronizedList()得到一个线程安全的ArrayList

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

也可以使用concurrent并发包下的CopyOnWriteArrayList类。

List<String> list = new CopyOnWriteArrayList<>();

36. CopyOnWriteArrayList

用于创建一个适用于并发的ArrayList,其主要实现原理是读写分离

写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。

写操作需要加锁,防止并发写入时导致写入数据丢失以及多线程写的时候会copy出多个副本出来。

写操作结束之后需要把原始数组指向新的复制数组。

public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}
static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}

适用场景

CopyOnWriteArrayList在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

但是CopyOnWriteArrayList有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

所以CopyOnWriteArrayList不适合内存敏感以及对实时性要求很高的场景。

37. LinkedHashMap分析

LinkedHashMap继承自HashMap,因此具有和HashMap一样快速查找的特性。

存储结构

LinkedHashMap内部维护了一个双向链表,用来维护插入顺序或者LRU顺序(最少使用淘汰算法)

/**
 * 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,此时维护的是插入顺序。

LinkedHashMap最重要的是以下用于维护顺序的函数:afterNodeAccess(),afterNodeInsertion()

afterNodeAccess():当一个节点被访问时,如果accessOrdertrue,则会将该节点移到链表尾部。也就是说指定为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

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);
    }
}

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

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

使用LinkedHashMap实现LRU缓存

思路如下:

  • 设定最大缓存空间MAX_ENTRIES
  • 使用LinkedHashMap的构造函数将accessOrder设置为true,开启URL顺序
  • 覆盖removeEldestEntry方法,在结点多于最大缓存空间的时候将最久未使用数据移除。
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);
    }
}

38. WeakHashMap

WeakHashMapEntry继承自WeakReference,被WeakReference关联的对象在下一次垃圾回收时会被回收。

WeakHashMap主要用来实现缓存,通过使用WeakHashMap来引用缓存对象,由JVM对这部分缓存进行回收。

Tomcat中的ConcurrentCache使用了WeakHashMap来实现缓存功能。

ConcurrentCache采取的是分代缓存:

  • 经常使用的对象放入eden中,eden使用ConcurrentHashMap实现,不用担心会被回收(伊甸园);
  • 不常用的对象放入longtermlongterm使用WeakHashMap实现,这些老对象会被垃圾收集器回收。
  • 当调用get()方法时,会先从eden区获取,如果没有找到的话再到longterm获取,当从longterm获取到就把对象放入eden中,从而保证经常被访问的节点不容易被回收。
  • 当调用put()方法时,如果eden的大小超过了 size,那么就将eden中的所有对象都放入longterm中,利用虚拟机回收掉一部分不经常使用的对象。
public final class ConcurrentCache<K, V> {

    private final int size;

    private final Map<K, V> eden;

    private final Map<K, V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            v = this.longterm.get(k);
            if (v != null)
                this.eden.put(k, v);
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            this.longterm.putAll(this.eden);
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

39. TreeSet

TreeSet也是基于Map来实现,具体实现是TreeMap,底层数据结构为红黑树。

与HashSet不同的是,TreeSet具有排序功能,分为自然排序(123456)和自定义排序两类,默认是自然排序;在程序中,可以按照任意顺序将元素插入到集合中,等到遍历时TreeSet会按照一定顺序输出–倒序或者升序;

它继承AbstractSet,实现NavigableSet, Cloneable, Serializable接口。

  • 与HashSet同理,TreeSet继承AbstractSet类,获得了Set集合基础实现操作;
  • TreeSet实现NavigableSet接口,而NavigableSet又扩展了SortedSet接口。这两个接口主要定义了搜索元素的能力,例如给定某个元素,查找该集合中比给定元素大于、小于、等于的元素集合,或者比给定元素大于、小于、等于的元素个数;简单地说,实现NavigableSet接口使得TreeSet具备了元素搜索功能;
  • TreeSet实现Cloneable接口,意味着它也可以被克隆;
  • TreeSet实现了Serializable接口,可以被序列化,可以使用hessian协议来传输;

TreeSet具有如下特点:

  • 对插入的元素进行排序,是一个有序的集合(主要与HashSet的区别);
  • 底层使用红黑树结构,而不是哈希表结构;
  • 允许插入Null值;
  • 不允许插入重复元素;
  • 线程不安全;

基本操作

public class TreeSetTest {
    public static void main(String[] agrs){
        TreeSet<String> treeSet = new TreeSet<String>();
        System.out.println("TreeSet初始化容量大小:"+treeSet.size());

        //元素添加:
        treeSet.add("my");
        treeSet.add("name");
        treeSet.add("jiaboyan");
        treeSet.add("hello");
        treeSet.add("world");
        treeSet.add("1");
        treeSet.add("2");
        treeSet.add("3");
        System.out.println("TreeSet容量大小:" + treeSet.size());
        System.out.println("TreeSet元素顺序为:" + treeSet.toString());

        //增加for循环遍历:
        for(String str:treeSet){
            System.out.println("遍历元素:"+str);
        }

        //迭代器遍历:升序
        Iterator<String> iteratorAesc = treeSet.iterator();
        while(iteratorAesc.hasNext()){
            String str = iteratorAesc.next();
            System.out.println("遍历元素升序:"+str);
        }

        //迭代器遍历:降序
        Iterator<String> iteratorDesc = treeSet.descendingIterator();
        while(iteratorDesc.hasNext()){
            String str = iteratorDesc.next();
            System.out.println("遍历元素降序:"+str);
        }

        //元素获取:实现NavigableSet接口
        String firstEle = treeSet.first();//获取TreeSet头节点:
        System.out.println("TreeSet头节点为:" + firstEle);

        // 获取指定元素之前的所有元素集合:(不包含指定元素)
        SortedSet<String> headSet = treeSet.headSet("jiaboyan");
        System.out.println("jiaboyan节点之前的元素为:"+headSet.toString());

        //获取给定元素之间的集合:(包含头,不包含尾)
        SortedSet subSet = treeSet.subSet("1","world");
        System.out.println("1--jiaboan之间节点元素为:"+subSet.toString());

        //集合判断:
        boolean isEmpty = treeSet.isEmpty();
        System.out.println("TreeSet是否为空:"+isEmpty);
        boolean isContain = treeSet.contains("who");
        System.out.println("TreeSet是否包含who元素:"+isContain);

        //元素删除:
        boolean jiaboyanRemove = treeSet.remove("jiaboyan");
        System.out.println("jiaboyan元素是否被删除"+jiaboyanRemove);
        
        //集合中不存在的元素,删除返回false
        boolean whoRemove = treeSet.remove("who");
        System.out.println("who元素是否被删除"+whoRemove);

       //删除并返回第一个元素:如果set集合不存在元素,则返回null
        String pollFirst = treeSet.pollFirst();
        System.out.println("删除的第一个元素:"+pollFirst);
        
        //删除并返回最后一个元素:如果set集合不存在元素,则返回null
        String pollLast = treeSet.pollLast();
        System.out.println("删除的最后一个元素:"+pollLast);


        treeSet.clear();//清空集合:
    }
}

元素排序

在TreeSet调用add方法时,会调用到底层TreeMap的put方法,在put方法中会调用到compare(key, key)方法,进行key大小的比较

40. Java中是如何支持正则表达式操作的?

Java 的 String 类中提供了支持正则表达式的操作,包括:matches(),replaceAll(),replaceFirst(),split()。此外,Java 中还可以使用 PatternMacher类进行正则表达式操作。

String str = "aaaaabbb";
str.matches("a*b");

Pattern p = Pattern.compile("a*b");//将正则表达式编译成Pattern类在内存中保存
Matcher m = p.matcher(str);//将执行匹配所涉及的状态保留再Matcher类中
boolean b = m.matches();//执行Matcher类的matches方法进行比较

41. Java中如何跳出嵌套循环?

在最外层循环前加一个标记如 A,然后用 break A ;可以跳出多重循环。(Java中支持带标签的 breakcontinue 语句,作用有点类似于C和C++中的 goto语句,但是就像要避免使用 goto 一样,应该避免使用带标签的 breakcontinue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好),根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。

42. & 和 && 的区别?

& 运算符有两种用法:

  • 按位与;
  • 逻辑与。

&& 运算符是短路与运算

逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是 true 整个表达式的值才是 true 。&& 之所以称为短路运算是因为,如果 && 左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用 && 而不是 &,例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用 & 运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。

43. Java里面final关键字的使用?

当用 final 修饰一个类时,表明这个类不能被继承。也就是说,如果一个类永远不会让他被继承,就可以用 final 进行修饰。final 类中的成员变量可以根据需要设为 final,但是要注意 final 类中的所有成员方法都会被隐式地指定为 final 方法

使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用 final 方法进行这些优化了。

对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

44. 如何通过反射创建对象以及获取和设置对象私有属性字段的值?

对象通过 getClass() 方法可以获得其 Class 对象,之后可以通过该 Class 对象的 getDeclaredFields() 方法获取包括 private 在内所有的属性;通过 getDeclaredMethods() 方法可以获取当前类包括 private 在内所有方法,但是不包括父类。之后通过setAccesssible(true)将其设置成可以访问(去除访问修饰符的检查)。

package reflection;

import java.lang.reflect.*;

public class ReflectionDemo {

    public static void main(String[] args) {
        Dog dog = new Dog("dabao",5,"白色","藏獒");
        Class dogClass = dog.getClass();
        //通过反射实例化对象
        try {
            //Dog inflectDog = (Dog)dogClass.newInstance("dwr");//jdk1.9之后不推荐使用
            Dog inflectDog = (Dog)dogClass.getDeclaredConstructor(String.class, int.class, String.class, String.class).newInstance("xiaobai",3,"黑色","咖啡毛");
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        //获取构造方法
        Constructor[] constructors = dogClass.getConstructors();//获取所有构造方法
        for (int i = 0; i < constructors.length; i++) {
            System.out.println(constructors[i].getName());//构造参数名
            System.out.println(constructors[i].getParameterCount());//参数数目
        }
        try {
            Constructor constructor = dogClass.getConstructor(String.class, int.class, String.class, String.class);//根据变量类型获取指定的构造方法
            //之后就可以调用上面的newInstance方法实例化一个对象
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }

        //获取类中的所有属性
        Field[] fields = dogClass.getFields();//获取所有非private的属性
        System.out.println(fields.length);
        Field[] declaredFields = dogClass.getDeclaredFields();//获取所有属性
        System.out.println(declaredFields.length);
        for (int i = 0; i < declaredFields.length; i++) {
            int modifiers = declaredFields[i].getModifiers();
            System.out.println(Modifier.toString(modifiers)+" "+declaredFields[i].getType()+" "+declaredFields[i].getName());
        }

        //获取包对象的包信息
        Package aPackage = dogClass.getPackage();
        System.out.println(aPackage.getName());

        //获取方法
        //获取所有非private方法,包括父类的方法
        Method[] methods = dogClass.getMethods();
        for (int i = 0; i < methods.length; i++) {
            System.out.println(methods[i]);
            if(methods[i].getName().equals("toString")){
                try {
                    String s = (String)methods[i].invoke(dog);//调用方法
                    System.out.println(s);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("-------------");
        //获取当前类的所有方法(包括private),不包括父类
        Method[] methods1 = dogClass.getDeclaredMethods();
        for (int i = 0; i < methods1.length; i++) {
            System.out.println(methods1[i]);
            if(methods1[i].getName().equals("set")){
                //设置私有方法可以被访问(去除访问修饰符的检查)
                methods1[i].setAccessible(true);
                try {
                    methods1[i].invoke(dog);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

45. 什么是范型?解释 extends 和 super 泛型限定符?

范型相当于将数据类型参数化,泛型只作用于编译期,在编译后会被所指定的具体类型代替。

  • 范型上限 < ? extends Fruit >:表示所能接收类型必须是指定类或者其子类。
  • 范型下限 < ? super Apple >:表示所能接收类型必须是指定类或者其父类。

注意点

上界的 list 只能使用 get 方法,而不能使用 add 方法(确切的说是不能 addnull 之外的对象,包括 Object)。

  • 原因:上界 <? extends Fruit>表示所有继承 Fruit 的子类,但是具体事哪个子类,无法确定,所以在调用 add 方法的时候,需要 add 什么类型是无法确定的。但是在 get 的时候,由于不管是什么子类,都能够通过向上转型使用 Fruit 作为接收对象。

下界的 list 只能 add,不能get

  • 原因:下界 <? super Apple> 表示 Apple 的所有父类,包括 Fruit,一直可以追溯到 Object。那么当使用 add 的时候,不能 add Apple 的父类,因为不能确定 List 中存放的到底是哪个父类,但是可以 add Apple 及其子类,因为他们都可以通过向上转型为 Apple 甚至是 Object。但是当使用 get 的时候,由于 Apple 有很多父类,并不知道要用什么类型接收返回值,所以无法使用。

46. String 为什么是不可变的?

不可变对象是指一个对象的状态在对象被创建之后就不再变化。不可改变的意思就是说:不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

String 不可变是因为在 JDK 中 String 类被声明为一个 final 类,且类内部的 value 字节数组也是 final 的,只有当字符串是不可变时字符串池才有可能实现,字符串池的实现可以在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串;如果字符串是可变的则会引起很严重的安全问题,譬如数据库的用户名密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中主机名和端口都是以字符串的形式传入,因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子改变字符串指向的对象的值造成安全漏洞;因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步,字符串自己便是线程安全的;因为字符串是不可变的所以在它创建的时候 hashcode 就被缓存了,不变性也保证了 hash 码的唯一性,不需要重新计算,这就使得字符串很适合作为 Map 的键,字符串的处理速度要快过其它的键对象,这就是 HashMap 中的键往往都使用字符串的原因。

47. Object 类中有哪些方法

getClass

public final native Class<?> getClass();

getClass 方法用于获取对象的运行时类,在反射的时候用的比较多。

注意到这个方法被 final 和 native 修饰,说明

  • 不能覆写
  • 实现在C/C++层

返回的 Class 对象就是表示类静态同步方法锁定的对象。

hashCode

public native int hashCode();

hashCode 方法用于获取对象的 hash 值。

在强烈依赖 hashCode 的地方是必须的。比如 HashMap 中对 key 进行 hash 计算,其实是利用 key 的 hashCode。

这个方法的实现依然在native层。

equals

public boolean equals(Object obj) {
    return (this == obj);
}

equals 方法用于比较两个对象是否相等。

默认的实现就是通过比较引用来判断是不是相等。

这种实现比较简单粗暴,首先引用相等就是同一个对象,肯定是相等的。但是在实际应用中引用不等,对象未必不相等。

clone

protected native Object clone() throws CloneNotSupportedException;

这个方法就是创建并返回一个对象的拷贝。
这个方法有三个值得注意的点:

  • 方式是 native 实现信息的拷贝。
  • 方法是 protected,外部没法调用。
  • 如果没有实现 Cloneable 接口,调用 的时候会抛出 CloneNotSupportedException 异常。

如果外部需要调用 clone 方法,要么通过反射,要么将它覆写成 public 方法。

这个 Cloneable 接口中没有定义任何方法,所以实现 Cloneable 接口没有别的作用,就代表具备了使用 clone 方法的权利。

toString

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

toString 方法就是把对象用字符串的形式表示,这个在日志中使用较多,当然也可以是类型转换,比如Integer 类。

默认的实现其实没有什么用,”类名@hashCode”,直接打印对象输出就是这样的格式。

官方文档建议最好覆写这个方法,返回一些有价值的、可读性强的信息。

wait(),notify(),notifyAll()

wait() 方法会将当前线程挂起,等待某个条件满足。当其他线程运行使得这个条件满足了,其他线程会调用 notify() 或者 notifyAll() 方法。

只能用在同步方法或者同步控制块中使用,获得当前对象的锁资源,否则会在运行时抛出 IllegalMonitorStateException。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
    if (timeoutMillis < 0) {
        throw new IllegalArgumentException("timeoutMillis value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeoutMillis++;
    }

    wait(timeoutMillis);
}

finalize

protected void finalize() throws Throwable { }

finalize 是 GC 准备回收对象的时候调用来执行清理工作的。(JVM 14题)

总结:

  • Object() 默认构造方法。
  • clone() 创建并返回此对象的一个副本。
  • equals(Object obj) 指示某个其他对象是否与此对象“相等”。
  • finalize() 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
  • getClass() 返回一个对象的运行时类。
  • hashCode() 返回该对象的哈希码值。
  • notify() 唤醒在此对象监视器上等待的单个线程。
  • notifyAll() 唤醒在此对象监视器上等待的所有线程。
  • toString() 返回该对象的字符串表示。
  • wait()导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
  • wait(long timeout) 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

48. List、Set、Map是否继承自Collection接口?

只有 List 和 Set 继承了 Collection 接口,Map 是键值对映射容器,不同于 Set 和 List 只存储元素。

49. Collection 和 Collections 的区别?

  • Collection 是集合类的上级接口,继承与他的接口主要有Set 和List。
  • Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

50. 与快速失败机制对应的安全失败机制

安全失败(fail-safe)采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception,例如 CopyOnWriteArrayList

缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

快速失败和安全失败是对迭代器而言的。

  • 快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出 ConcurrentModification异常java.util 下都是快速失败。
  • 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在 java.util.concurrent 下都是安全失败

51. Iterator 和 ListIterator 区别?

Iterator 可用来遍历 SetList 集合,但是 ListIterator 只能用来遍历 List

Iterator 对集合只能是前向遍历,ListIterator 既可以前向也可以后向。

ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。

Iterator 提供了统一遍历操作集合元素的统一接口, Collection 接口实现 Iterable 接口,
每个集合都通过实现 Iterable 接口中 iterator() 方法返回 Iterator 接口的实例, 然后对集合的元素进行迭代操作。

有一点需要注意的是:在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出 ConcurrentModificationException 异常. 但是可以通过 Iterator 接口中的 remove() 方法进行删除。(详见24题)。

52. 运行时异常和受检异常有什么区别?

运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见的运行错误,只要程序设计得当通常就不会发生。当出现这样的异常,可以不处理,总是由虚拟机接管。RuntimeException下的类便都是运行时异常。

受检异常跟程序的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而导致异常。Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。

53. JDBC 使用流程

  1. 注册 JDBC 驱动(Class.forName("com.mysql.jdbc.Driver")
  2. 打开连接(DriverManager.getConnection("url","name","password")
  3. 根据连接,创建 Statement(conn.prepareStatement(sql)
  4. 设置参数(stmt.setString(1, "wyf");
  5. 执行查询(stmt.executeQuery();
  6. 处理结果,结果集映射(resultSet.next()
  7. 关闭资源(finally

54. Java提供的排序算法是怎么实现的?

Arrays.sort()的排序算法

如果数组长度大于等于286且连续性好的话,就用归并排序,如果大于等于286且连续性不好的话就用双轴快速排序。如果长度小于286且大于等于47的话就用双轴快速排序,如果长度小于47的话就用插入排序。

Collections.sort()的排序算法

LegacyMergeSort.userRequested为 true 的话就会使用归并排序,可以通过下面代码设置为true:

System.setProperty("java.util.Arrays.useLegacyMergeSort","true");

不为 true 的话就会用一个叫 TimSort 的排序算法。

55. Collections.synchronizedMap()与 ConcurrentHashMap 的区别

SynchronizedMap 是一个实现了 Map 接口的代理类,该类中对 Map 接口中的方法使用 synchronized 同步关键字来保证对 Map 的操作是线程安全的。

Collections.synchronizedMap() 与 ConcurrentHashMap 主要区别是:

Collections.synchronizedMap() 和 Hashtable 一样,实现上在调用 map 所有方法时,都对整个 map 进行同步,会抛出 java.util.ConcurrentModificationException 异常,而 ConcurrentHashMap 的实现却更加精细,在 ConcurrentHashMap中,每个桶都有一个自己对应的锁。

所以,只要有一个线程访问 synchronizedMap,其他线程就无法进入 map,而如果一个线程在访问 ConcurrentHashMap 某个桶时,其他线程,仍然可以对 map 执行某些操作。这样,ConcurrentHashMap 在性能以及安全性方面,明显比 Collections.synchronizedMap() 更加有优势。同时,同步操作精确控制到桶,所以,即使在遍历 map 时,其他线程试图对 map 进行数据修改,也不会抛出 ConcurrentModificationException。

不论 Collections.synchronizedMap() 还是 ConcurrentHashMap 对 map 同步的原子操作都是作用的 map 的方法上,map 在读取与清空之间,线程间是不同步的。

还有一个区别是:ConcurrentHashMap 从类的命名就能看出,它必然是个 HashMap。而Collections.synchronizedMap() 可以接收任意Map实例,实现Map的同步。

56. JAVA 反射的原理

反射调用过程如下:

Class actionClass=Class.forName("MyClass");
Object action=actionClass.newInstance();
Method method = actionClass.getMethod("myMethod",null);
method.invoke(action,null);

首先是 forName 方法

@CallerSensitive
public static Class<?> forName(String className)
        throws ClassNotFoundException {
    // 先通过反射,获取调用进来的类信息,从而获取当前的 classLoader            
    Class<?> caller = Reflection.getCallerClass();
    // 调用native方法进行获取class信息
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

forName()反射获取类信息,并没有将实现留给了 java,而是交给了 jvm 去加载。主要是先获取 ClassLoader, 然后调用 native 方法,获取信息,加载类则是回调 java.lang.ClassLoader。最后,jvm 又会回调 ClassLoader 进行类加载过程。

接下来是 newInstance 方法

newInstance() 主要做了三件事:

  1. 权限检测,如果不通过直接抛出异常;
  2. 查找无参构造器,并将其缓存起来,这个过程分三步:
  3. 调用具体方法的无参构造方法,生成实例并返回

在查询无参构造器的时候,使用 getConstructor0() 为获取匹配的构造方器,分三步;

  1. 先获取所有的 constructors, 然后通过进行参数类型比较;
  2. 找到匹配后,通过 ReflectionFactory copy一份 constructor 返回;
  3. 否则抛出 NoSuchMethodException;

而获取所有的构造器主要步骤,都在 privateGetDeclaredConstructors() 方法里:

  1. 先尝试从缓存中获取;
  2. 如果缓存没有,则从 jvm 中重新获取,并存入缓存,缓存使用软引用进行保存,保证内存可用;

反射获取方法 getMethod(“myMethod”) 获得 Method 对象

  1. 获取所有方法列表;
  2. 根据方法名称和方法列表,选出符合要求的方法;
  3. 如果没有找到相应方法,抛出异常,否则返回对应方法;

在获取所有方法列表的过程中,首先会从缓存中去寻找。

匹配到方法名后,进行参数类型匹配,但是,匹配到一个方法,并没有退出 for 循环,而是继续进行匹配。这是因为进行最优匹配,匹配最精确的子类进行返回。最后,通过 ReflectionFactorycopy 方法后返回!

Method 对象生成过程

每个类都会有一个与之对应的 Class 实例,JVM 管理着这个 Class 实例,这个实例里维护着该类的所有 Method,Field,Constructor 的 cache,这份 cache 也可以被称作根对象。每次 getMethod 获取到的 Method 对象都持有对根对象的引用,因为一些重量级的 Method 的成员变量(主要是 MethodAccessor),我们不希望每次创建 Method 对象都要重新初始化,于是所有代表同一个方法的 Method 对象都共享着根对象的 MethodAccessor,每一次创建都会调用根对象的 copy 方法复制一份

Method.invoke() 反射调用方法

反射调用 invoke 方法流程

调用 Method.invoke 之后,会直接去调 MethodAccessor.invoke,最终是由 jvm 执行 invoke0() 执行。MethodAccessor 就是上面提到的所有同名 method 共享的一个实例,由 ReflectionFactory 创建。

创建机制采用了一种名为 inflation 的方式(JDK1.4之后):如果该方法的累计调用次数 <=15,会创建出NativeMethodAccessorImpl,它的实现就是直接调用 native 方法实现反射;如果该方法的累计调用次数 >15,会由 java 代码创建出字节码组装而成的 MethodAccessorImpl。(是否采用 inflation 和 15 这个数字都可以在 jvm 参数中调整)

57. JVM、JRE、JDK 的区别?

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

什么是字节码?采用字节码的好处是什么?

在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

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

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

如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。

58. JAVA的简单数据类型和复合数据类型

基本数据类型:

  • byte/8(1个字节)
  • char/16(2个字节)
  • short/16(2个字节)
  • int/32(4个字节)
  • float/32(4个字节)
  • long/64(8个字节)
  • double/64(8个字节)
  • boolean/1(1位)

复合数据类型:

Java 虚拟机(JVM)还定义了索引(reference)这种数据类型。索引类型可以“引用”变量,由于 Java 没有明确地定义指针类型,所以索引类型可以被认为就是指向实际值或者指向变量所代表的实际值的指针。一个对象可以被多于一个以上的索引所“指”。JVM从不直接对对象寻址而是操作对象的索引。

索引类型分成三种,它们是:类(class)、接口(interface)和数组(array)。索引类型可以引用动态创建的类实例、普通实例和数组。索引还可以包含特殊的值,这就是 null 索引。null 索引在运行时上并没有对应的类型,但它可以被转换为任何类型。索引类型的默认值就是 null。

59. 字符编码

字符集(Charset):是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

字符编码(Character Encoding):是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。即在符号集合与数字系统之间建立对应关系,它是信息处理的一项基本技术。通常人们用符号集合(一般情况下就是文字)来表达信息。而以计算机为基础的信息处理系统则是利用元件(硬件)不同状态的组合来存储和处理信息的。元件不同状态的组合能代表数字系统的数字,因此字符编码就是将符号转换为计算机可以接受的数字系统的数,称为数字代码。

常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。

ASCII 码

在计算机内部,所有信息最终都是一个二进制值。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。

上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。

ASCII 码一共规定了128个字符的编码,比如空格SPACE是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0。

Unicode

世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode,就像它的名字都表示的,这是一种所有符号的编码。

Unicode 是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字严。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。

Unicode 的问题

需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

比如,汉字严的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?

第二个问题是,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

它们造成的结果是:

  • 出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。
  • Unicode 在很长一段时间内无法推广,直到互联网的出现。

UTF-8

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。

UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8 的编码规则很简单,只有二条:

1)对于单字节的符号:字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的;
2)对于n字节的符号(n > 1):第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

下表总结了编码规则,字母x表示可用编码的位:

UTF-8编码规则

跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

下面,还是以汉字严为例,演示如何实现 UTF-8 编码。

严的 Unicode 是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,向右对齐依次填入格式中的x,左边多出的x位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5。

Little endian 和 Big endian

window 中的应用 notepad.exe 有一个 UCS-2 编码方式:即直接用两个字节存入字符的 Unicode 码

UCS-2 格式可以存储 Unicode 码(码点不超过0xFFFF)。以汉字严为例,Unicode 码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,这就是 Big endian 方式;25在前,4E在后,这是 Little endian 方式。

第一个字节在前,就是”大头方式”(Big endian),第二个字节在前就是”小头方式”(Little endian)。

那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(zero width no-break space),用FEFF表示。这正好是两个字节,而且FF比FE大1。

如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

GB系列字符集&编码

计算机发明之处及后面很长一段时间,只用应用于美国及西方一些发达国家,ASCII能够很好满足用户的需求。但是当天朝也有了计算机之后,为了显示中文,必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。

天朝专家把那些 127 号之后的奇异符号们(即EASCII)取消掉,规定:一个小于 127 的字符的意义与原来相同,但两个大于 127 的字符连在一起时,就表示一个汉字,前面的一个字节(称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样就可以组合出大约 7000 多个简体汉字了。在这些编码里,还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在 127 号以下的那些就叫”半角”字符了。

上述编码规则就是GB2312。

GB2312或GB2312-80 是中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。GB2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。对于人名、古汉语等方面出现的罕用字,GB2312不能处理,这导致了后来GBK及GB 18030汉字字符集的出现。

由于 GB 2312-80 只收录 6763 个汉字,有不少汉字,如部分在 GB 2312-80 推出以后才简化的汉字(如”啰”),部分人名用字(如中国前总理朱镕基的”镕”字),台湾及香港使用的繁体字,日语及朝鲜语汉字等,并未有收录在内。于是厂商微软利用 GB 2312-80 未使用的编码空间,收录 GB 13000.1-93 全部字符制定了 GBK 编码。根据微软资料,GBK 是对 GB2312-80 的扩展,也就是 CP936 字码表 (Code Page 936)的扩展(之前CP936和GB 2312-80一模一样),最早实现 于Windows 95 简体中文版。虽然 GBK 收录GB 13000.1-93 的全部字符,但编码方式并不相同。GBK 自身并非国家标准,只是曾由国家技术监督局标准化司、电子工业部科技与质量监督司公布为”技术规范指导性文件”。原始 GB13000 一直未被业界采用,后续国家标准 GB18030 技术上兼容 GBK 而非 GB13000。

GB 18030,全称:国家标准GB 18030-2005《信息技术 中文编码字符集》,是中华人民共和国现时最新的内码字集,是GB 18030-2000《信息技术 信息交换用汉字编码字符集 基本集的扩充》的修订版。与GB 2312-1980完全兼容,与GBK基本兼容,支持GB 13000及Unicode的全部统一汉字,共收录汉字70244个。

GB 18030主要有以下特点:

  • 与UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成;
  • 编码空间庞大,最多可定义161万个字符;
  • 支持中国国内少数民族的文字,不需要动用造字区;
  • 汉字收录范围包含繁体汉字以及日韩汉字。

各个编码之间的兼容关系如下:

各个编码之间的兼容关系

60. Lambda 表达式实现原理

针对如下代码:

public class LambdaTest {
    public static void printString(String s, Print<String> print) {
        print.print(s);
    }
    public static void main(String[] args) {
        printString("test", (x) -> System.out.println(x));
    }
}

@FunctionalInterface
interface Print<T> {
    public void print(T x);
}

使用 javac 编译之后再用 javap -p 进行反编译,可以得到如下代码:

C:\Users\Code\Java\study>javap -p LambdaTest.class
Compiled from "LambdaTest.java"
public class LambdaTest {
  public LambdaTest();
  public static void printString(java.lang.String, Print<java.lang.String>);
  public static void main(java.lang.String[]);
  //生成的私有静态方法
  private static void lambda$main$0(java.lang.String);
}

由上面的代码可以看出编译器会根据Lambda表达式生成一个私有的静态函数:

private static void lambda$main$0(java.lang.String);

由此可以直到 Lambda 表达式在 Java 9 中首先会生成一个私有的静态函数,这个私有的静态函数干的就是 Lambda 表达式里面的内容,那么又是如何调用的生成的私有静态函数(lambda$main$0(String s))呢?

通过 javap -p -v -c LambdaTest.class 查看完整的反编译结果,比较重要的有以下几段:

反编译后的 main 方法:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         // ldc 这个操作码用来将常量从运行时常量池压栈到操作数栈
         0: ldc           #3                  // String test
         // 注意下面两句:通过实例调用 print
         2: invokedynamic #4,  0              // InvokeDynamic #0:print:()LPrint;        
         //调用静态方法 printString
         7: invokestatic  #5                  // Method printString:(Ljava/lang/String;LPrint;)V
        10: return

invokeddynamic 指令表示动态调用指令,动态类型只有在运行期才能确定接收者类型。每一个 invokedynamic 指令的实例叫做一个动态调用点(dynamic call site), 动态调用点最开始是未链接状态(unlinked:表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法。引导方法是由编译器生成, 在运行期当 JVM 第一次遇到 invokedynamic 指令时, 会调用引导方法来将 invokedynamic 指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为.引导方法的返回值类型是 java.lang.invoke.CallSite, 一个 invokedynamic 指令关联一个 CallSite, 将所有的调用委托到 CallSite 当前的 target(MethodHandle)

上面涉及到通过实例调用,而实例如下:

InnerClasses:
     public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #29 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:
  (Ljava/lang/invoke/MethodHandles$Lookup;
  Ljava/lang/String;Ljava/lang/invoke/MethodType;
  Ljava/lang/invoke/MethodType;
  Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)
  Ljava/lang/invoke/CallSite;
  Method arguments:
      //对象类型终结符为 L 和 ;
      //Object V
      #30 (Ljava/lang/Object;)V
      #31 invokestatic LambdaTest.lambda$main$0:(Ljava/lang/String;)V
      #32 (Ljava/lang/String;)V

会看到生成这个实例的内部类,可以通过以下指令运行,运行时,会将生成的内部类 class 码输出到一个文件中

java -Djdk.internal.lambda.dumpProxyClasses LambdaTest

再通过通过 jad 反编译 LambdaTest$$Lambda$1.class 文件,内容如下:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
final class LambdaTest$$Lambda$1 implements Print {
    private LambdaTest$$Lambda$1() {
    }

    public void print(Object obj) {
        LambdaTest.lambda$main$0((String) obj);
    }
}

可以看到,最终使用 Lambda 表达式的代码最终的执行代码如下:

public class LambdaTest {
    public static void PrintString(String s, Print<String> print) {
        print.print(s);
    }

    public static void main(String[] args) {
        PrintString("test", new LambdaTest$$Lambda$1());
    }

    private static void lambda$main$0(String x) {
        System.out.println(x);
    }

    static final class LambdaTest$$Lambda$1 implements Print {
        public void print(Object obj) {
            LambdaTest.lambda$main$0((String) obj);
        }
        private LambdaTest$$Lambda$1() {
        }
    }

}

@FunctionalInterface
interface Print<T> {
    public void print(T x);
}

所以,其实现原理如下:

  1. 在类编译时,虚拟机通过 invokedynamic 指令动态创建相应的实现类
  2. 会生成一个私有静态方法和一个内部类
  3. 在使用 lambda 表达式的地方,通过传递内部类实例,来调用私有静态方法。

参考内容

主要参考以来两篇博客以及相关博客推荐,因找的博客比较多,没注意记录,最后好多忘了在哪2333,如果有侵权,请及时联系我,非常抱歉。
https://github.com/Snailclimb/JavaGuide
https://github.com/CyC2018/CS-Notes
深入理解java反射原理
Java反射原理简析
为什么HashMap线程不安全
深入解读Object类
为什么ConcurrentHashMap的读操作不需要加锁?
一文读懂Java ConcurrentHashMap原理与实现
JAVA的简单数据类型和复合数据类型
小结-JAVA中的复合数据类型
程序员必备:彻底弄懂常见的7种中文字符编码
字符编码那点事:快速理解ASCII、Unicode、GBK和UTF-8
Java 8 动态类型语言Lambda表达式实现原理分析
Java Lambda表达式 实现原理分析