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

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 深拷贝和浅拷贝

图示区别

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

浅拷贝:

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();
        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.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. 多线程问题:

    在多线程情况中,读取不会有安全性问题。但是在写入的情况下,就可能会出现各种竞争的问题。

  2. 安全性问题:

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

2.4.2 StringBuilder & StringBuffer

StringBuffer 是线程安全的 StringBuilder 是不安全的。

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

2.5 泛型

通用的泛型标准:

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

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

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

但是 Java 的泛型并不是真正的泛型,在编译时,泛型中的类型会被擦除。

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

2.6 Java 位运算

在 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.7 static & final

static:

  1. 为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间;
  2. 使得某个方法或属性与类而不是对象关联在一起,即在不创建对象的情况下可通过类直接调用方法或使用类的属性。

final:

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

Java 代码执行顺序:

  1. 父类静态代码块(只执行一次);
  2. 子类静态代码块(只执行一次);
  3. 父类构造代码块;
  4. 父类构造函数;
  5. 子类构造代码块;
  6. 子类构造函数;
  7. 普通代码块。

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 集合

3.2.1 Java List

ArrayList & Vector

  • ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
  • VectorList 的古老实现类,底层使用Object[ ] 存储,线程安全的。

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

建议项目中不要使用 LinkedList,下面是 LinkedList 作者的推特内容。

LinkedList 开发者在 twitter 上的回答

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

CopyOnWrite

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

CopyOnWriteArrayList 适用于读多写少的场景。

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

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

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 扩容机制

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)。
     * 在jdk1.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;
    }
}

// 在源码中可以看到,每次需要扩容的时候,新数组都会扩展到原数组的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 分析

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 重写序列化和反序列化方法,保证只序列化到实际储存的元素,而不是整个数组,避免了不必要的浪费。

3.2.2 Java Set

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

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

  • HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。

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

  • TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。

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

3.2.3 Java Map

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、死锁等等问题。

HashTable

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

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

Hashtable 全表锁

ConcurrentHashMap

在 JDK 1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。

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

JDK 1.7 ConcurrentHashMap

ConcurrentHashMap 取消了 Segment 分段锁,采用 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。

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

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 要做的事情,性能比直接的 Java 代码要慢很多;

  • 安全问题:让我们可以动态操作改变类的属性同时也增加了类的安全隐患。

反射使用:

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

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()

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 表达式
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() 在没有值的时候返回的第二种情况
// orElseGet() 可以接受一个lambda表达式生成默认值
// System.out.println(empty.orElseGet(() -> "Default Value"));
// orElseThrow() 例子:empty.orElseThrow(ValueAbsentException::new);

// filter 与 stream 用法类似

// map 也提供类型转换,map 方法中的 lambda 表达式返回值可以是任意类型,在 map 函数返回之前会包装为 Optional
// flatMap 方法中的 lambda表达式返回值必须是 Optionl 实例 

5.2 JDK 11

参考文章

  1. Java 学习+面试整理
  2. 轻松看懂 Java 字节码
  3. UTF-8 到底是什么意思
  4. 后端八股文整合
  5. 为什么这么多人不喜欢用 goto?
  6. Java 序列化与反序列化
  7. JDK 动态代理和 CGLib 动态代理的对比
  8. ArrayList 的扩容机制
  9. HashMap 的实现原理
  10. 为什么 HashMap 的加载因子是0.75?
  11. Java 中的 String 为什么要设计成不可变的?
  12. 为什么 HashTable 被弃用了
  13. HashTable 和 Vector 为什么逐渐被废弃
  14. Java CopyOnWriteArrayList 详解
  15. Java HashMap的死循环
  16. HashMap? ConcurrentHashMap?
  17. Java 中的 transient 关键字详解
  18. Java 基础八股文背诵版
  19. ConcurrentHashMap,分段锁,CAS
  20. Java 动态代理
  21. Java 锁与线程的那些事
  22. Java 并发编程:Synchronized 底层优化(偏向锁、轻量级锁)
  23. Java Supplier

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