面试:Java基础知识点

记录Java中的一些小知识点。

主要内容出自javaGuideJava-concurrency

一、泛型相关

Java泛型类型擦除以及类型擦除带来的问题

JAVA 泛型中的通配符 T,E,K,V,?

二、==、equals与hashcode

  • 基本数据类型==比较的是值,引用数据类型==比较的是内存地址

  • Object.equals()等价于"==",对地址进行判断

    public boolean equals(Object obj) {
         return (this == obj);
    }
    
  • 对象内容判断是否相等,需要重写equals()方法。String、Integer已经重写过equals()方法

    重写equals()方法有以下要求:
    1、对称性:如果x.equals(y)返回是"true",那么y.equals(x)也应该返回是"true"。
    2、自反性:x.equals(x)必须返回是"true"。
    3、传递性:如果x.equals(y)返回是"true",而且y.equals(z)返回是"true",那么z.equals(x)也应该返回是"true"。
    4、一致性:如果x.equals(y)返回是"true",只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是"true"。
    5、非空性,x.equals(null),永远返回是"false";x.equals(和x不同类型的对象)永远返回是"false"。

  • 关于重写equals()方法需要重写hashCode()方法

    Java hashCode() 和 equals()的若干问题解答
    1、不会创建类对应的散列表
    我们不会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类。例如,不会创建该类的HashSet集合。
    在这种情况下,该类的“hashCode() 和 equals() ”没有关系的
    2、会创建“类对应的散列表”
    在这种情况下。若要判断两个对象是否相等,除了要覆盖equals()之外,也要覆盖hashCode()函数。否则,equals()无效。

三、基本数据类型运算的一些例子

Byte,Short,Integer,Long 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,
Character 创建了数值在[0,127]范围的缓存数据,
Boolean 直接返回 True Or False。
如果超出对应范围仍然会去创建新的对象。

  Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);

  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));//true,原因:编译器常量折叠优化
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));//true

语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。

四、String、StringBuilder、StringBuffer

  • String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以String 对象是不可变的。

  • StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[] value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

  • StringBuffer线程安全,StringBuilder线程不安全

五、Object.clone()

Object.clone()是浅拷贝,要实现深拷贝有两种方法:
1、让每个引用类型属性内部都重写clone() 方法
2、利用序列化-反序列化
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。

六、异常

在这里插入图片描述
在这里插入图片描述

七、try-catch-finally

无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

在以下 3 种特殊情况下,finally 块不会被执行:

1、在 tryfinally 块中用了 System.exit(int)退出程序。
   但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行
2、程序所在的线程死亡。
3、关闭 CPU。
public class Test {
    public static int f(int value) {
        try {
            return value * value;
        } finally {
            if (value == 2) {
                return 0;
            }
        }
    }
}

f(2)=0, finally语句的返回值覆盖了 try 语句块的返回值。

public static int inc(){
        int x;
        try{
            x=1;
            return x;
        }catch (Exception e){
            x = 2;
            return x;
        }finally {
            x = 3;
        }
    }

结果为1

八、Thread.sleep()没有释放锁,而Object.wait()释放了锁

九、static

static用于以下场景

  • 修饰成员变量和成员方法
  • 静态代码块
  • 静态内部类
  • 静态导包

静态代码块

执行顺序 静态代码块->非静态代码块->构造方法
如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问.

public class testOne extends TestTwo{
    public testOne(){
        System.out.println("子类构造方法");
    }
    {
        System.out.println("子类代码块");
    }
    static {
        System.out.println("子类静态代码块");
    }

    public static void main(String[] args) {
        new testOne();
    }

}
class  TestTwo{
    public TestTwo(){
        System.out.println("父类构造方法");
    }
    {
        System.out.println("父类代码块");
    }
    static {
        System.out.println("父类静态代码块");
    }
    public static void find(){
        System.out.println("静态方法");
    }
}

父类静态代码块
子类静态代码块
父类代码块
父类构造方法
子类代码块
子类构造方法
public class Demo {

    public static void main(String[] args) {
        B b = new B();
    }
}

class A{
    private static A a = new A();

    static {
        System.out.println("static A");
    }

    A(){
        System.out.println("construct A");
    }

    {
        System.out.println("init A");
    }

}

class B extends A{
    private static B b = new B();

    B(){
        System.out.println("construct B");
    }

    static {
        System.out.println("static B");
    }

    {
        System.out.println("init B");
    }
}

init A
construct A
------------
static A
init A
construct A
------------
init B
construct B
------------
static B
init A
construct A
init B
construct B

静态内部类

非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,
但是静态内部类却没有该引用。意味着:

  • 它的创建是不需要依赖外围类的创建。
  • 它不能使用任何外围类的非static成员变量和方法。
//静态内部类实现单例模式
public class Singleton {
    
    //声明为 private 避免调用默认构造方法创建对象
    private Singleton() {
    }
    
   // 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

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

十、Comparable和Comparator的区别

底层采用归并排序实现

Comparator排序源码分析

java基础-Comparator接口与Collections实现排序算法

(1)Comparable

Comparable可以认为是一个内比较器,实现了Comparable接口的类有一个特点,就是这些类是可以和自己比较的,至于具体和另一个实现了Comparable接口的类如何比较,则依赖compareTo方法的实现,compareTo方法也被称为自然比较方法。

public class Domain implements Comparable<Domain>
{
    public int compareTo(Domain domain) {... }
	...
}

d1.compareTo(d2);

前面说实现Comparable接口的类是可以支持和自己比较的,但是其实代码里面Comparable的泛型未必就一定要是Domain,将泛型指定为String或者指定为其他任何任何类型都可以----只要开发者指定了具体的比较算法就行。

(2)Comparator

Comparator可以认为是是一个外比较器

因为泛型指定死了,所以实现Comparator接口的实现类只能是两个相同的对象(不能一个Domain、一个String)进行比较了,因此实现Comparator接口的实现类一般都会以"待比较的实体类+Comparator"来命名

public class DomainComparator implements Comparator<Domain>
{
    public int compare(Domain domain1, Domain domain2) {...}
}

dc.compare(d1, d2);

1、如果实现类没有实现Comparable接口,又想对两个类进行比较(或者实现类实现了Comparable接口,但是对compareTo方法内的比较算法不满意),那么可以实现Comparator接口,自定义一个比较器,写比较算法

2、实现Comparable接口的方式比实现Comparator接口的耦合性 要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修 改。从这个角度说,其实有些不太好,尤其在我们将实现类的.class文件打成一个.jar文件提供给开发者使用的时候。实际上实现Comparator 接口的方式后面会写到就是一种典型的策略模式。

Comparable和Comparator的区别

十一、反射原理

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

native方式初始化更快。随着调用次数的增加,每次反射都使用JNI(Java Native Interface)跨越native边界会对优化有阻碍作用,相对来说使用拼装出的字节码可以直接以Java调用的形式实现反射,发挥了JIT优化的作用,避免了JNI为了维护OopMap(HotSpot用来实现准确式GC的数据结构)进行封装/解封装的性能损耗。因此在已经创建了MethodAccessor的情况下,使用Java版本的实现会比native版本更快。所以当调用次数到达一定次数(15次)后,会切换成Java实现的版本,来优化未来可能的更频繁的反射调用。

Java反射原理简析

深入理解java反射原理

《深入理解Java虚拟机》- JVM是如何实现反射的

JAVA反射中获取CLASS对象三种方式的区别?

1、new Object().getClass
2、Object.class
3、Class.forName(“java.util.String”)

public class Person {
    static {
        System.out.println("Person:静态代码块");
    }
    {
        System.out.println("Person:动态代码块");
    }
    public Person(){
        System.out.println("Person:构造方法");
    }
}

@Test
public void test4() throws ClassNotFoundException {
    Class<?> clz = Person.class;
    System.out.println("---------------");
    clz = Class.forName("com.choupangxia.reflect.Person");
    System.out.println("---------------");
    clz = new Person().getClass();
}

---------------
Person:静态代码块
---------------
Person:动态代码块
Person:构造方法

(1)类名.class:JVM将使用类装载器,将类装入内存(前提是:类还没有装入内存),不做类的初始化工作,返回Class的对象。

(2)Class.forName(“类名字符串”):装入类,并做类的静态初始化,返回Class的对象。

(3)实例对象.getClass():对类进行静态初始化、初始化;返回引用运行时真正所指的对象(子对象的引用会赋给父对象的引用变量中)所属的类的Class的对象。

说说JAVA反射中获取CLASS对象三种方式的区别?

十二、IO模型

应用程序发起的一次IO操作实际包含两个阶段:

  • IO调用阶段:应用程序进程向内核发起系统调用
  • IO执行阶段:内核执行IO操作并返回
    1、 准备数据阶段:内核等待I/O设备准备好数据
    2、 拷贝数据阶段:将数据从内核缓冲区拷贝到用户空间缓冲区

在这里插入图片描述
多路复用IO 为何比非阻塞IO 模型的效率高?

因为在非阻塞IO 中,不断地询问socket 状态时通过用户线程去进行的,而在多路复用IO 中,轮询每个socket 状态是内核在进行的,这个效率要比用户线程要高的多。不过要注意的是,多路复用IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

常见JAVA IO/NIO模型
在这里插入图片描述

IO多路复用之select/poll

Select是内核提供的系统调用,它支持一次查询多个系统调用的可用状态,当任意一个结果状态可用时就会返回,用户进程再发起一次系统调用进行数据读取。换句话说,就是NIO中N次的系统调用,借助Select,只需要发起一次系统调用就够了。其IO流程如下所示:
在这里插入图片描述
但是,select有一个限制,就是存在连接数限制,针对于此,又提出了poll。其与select相比,主要是解决了连接限制。

select/epoll 虽然解决了NIO重复无效系统调用用的问题,但同时又引入了新的问题。问题是:

  1. 用户空间和内核空间之间,进行大量的文件描述符拷贝
  2. 内核循环遍历IO状态,浪费CPU时间

换句话说,select/poll虽然减少了用户进程的发起的系统调用,但内核的工作量只增不减。在高并发的情况下,内核的性能问题依旧。所以select/poll的问题本质是:内核存在无效的循环遍历。

IO多路复用之epoll

针对select/pool引入的问题,我们把解决问题的思路转回到内核上,如何减少内核重复无效的循环遍历呢?变主动为被动,基于事件驱动来实现。其流程图如下所示:

在这里插入图片描述
epoll相较于select/poll,多了两次系统调用,其中epoll_create建立与内核的连接,epoll_ctl注册事件,epoll_wait阻塞用户进程,等待IO事件。
在这里插入图片描述

IO 模型知多少 | 理论篇

服务器网络编程之 IO 模型

1、select、poll、epoll

select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。

select vs poll

  • select 的描述符类型使用数组实现,有数量限制,默认大小为 1024。poll没有数量限制
  • poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
  • 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。
  • select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
  • 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll

epoll

  • 仅适用于 Linux OS
  • epoll 比 select 和 poll 更加灵活而且没有描述符数量限制
  • epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符
  • 一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。

工作模式

  1. LT 模式
    当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
  2. ET 模式
    通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

应用场景

1、 select 应用场景

select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。

select 可移植性更好,几乎被所有主流平台所支持

2.、poll 应用场景

poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。

3、 epoll 应用场景

只能运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接

需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。

需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试

2、Java中的NIO

NIO 与普通 I/O 的区别主要有以下两点:

  • NIO 是非阻塞的;
  • NIO 面向块,I/O 面向流
(1)通道

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。

通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。

(2)缓冲区

不会直接对通道进行读写数据,而是要先经过缓冲区。

缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区状态变量

  • capacity:最大容量;
  • position:当前已经读写的字节数;
  • limit:还可以读写的字节数。
    在这里插入图片描述
    在这里插入图片描述
(3)选择器

NIO 实现了 IO 多路复用中的 Reactor 模型一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件

通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。

在这里插入图片描述

(4)内存映射文件

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的

十三、容器

TreeSet 底层使用红黑树,能够按照排序的顺序进行遍历,排序的方式有自然排序和定制排序。

1、 HashMap 和 Hashtable 的区别

1、线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。
2、效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
3、对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
4、初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍
② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小。
5、底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度>=阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

2、 ArrayList

(1)modCount 用于Fail-Fast

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

(2)初始化
  • 有参构造方法创建时会创建指定大小容量的数组

  • 无参数构造方法创建 ArrayList时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10(默认值)

	/**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组(用于空实例)。主要用于传入的initialCapacity=0的情况
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

     //用于默认大小空实例的共享空数组实例。 主要用于无参构造函数的初始化,为了与空数组区分
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存ArrayList数据的数组
     */
    transient Object[] elementData; // non-private to simplify nested class access
	/**
     * 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //如果传入的参数大于0,创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果传入的参数等于0,创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //其他情况,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     *默认无参构造函数
     *DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
     */
    public ArrayList(Collection<? extends E> c) {
        //将指定集合转换为数组
        elementData = c.toArray();
        //如果elementData数组的长度不为0
        if ((size = elementData.length) != 0) {
            // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)
            if (elementData.getClass() != Object[].class)
                //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 其他情况,用空数组代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
(3)扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);

过程总结:

  1. 如果elementData等于初始化时创建的空数组,则minCapacity取默认的容量和minCapacity的最大值
  2. 如果minCapacity大于elementData的长度,进行扩容
  3. 首先计算新容量,新容量为原来的1.5倍左右
  4. 然后检查新容量是否大于minCapacity,如果小于minCapacity,那么就把minCapacity当作数组的新容量
  5. 如果新容量大于 MAX_ARRAY_SIZE,然后来比较 minCapacityMAX_ARRAY_SIZE的大小,
  6. minCapacity大于MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE
	//得到最小扩容量
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
	//判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }
	/**
     * 要分配的最大数组大小
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ArrayList扩容的核心方法。
     */
    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
       //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
        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);
    }
private static int hugeCapacity(int minCapacity) {
  	if (minCapacity < 0) // overflow
         throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;
}
(4)ensureCapacity可减少扩容次数
    /**
     * 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
     * @param   minCapacity   所需的最小容量
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table,给不是用无参构造函数创建的list准备的
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size. 默认的初始化空数组,低于默认值无效
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }
(5) ArrayList 自定义了序列化与反序列化,只序列化了被使用的数据

3、Vector

(1)初始化

VectorArrayList代码差不多,不同的是Vector没有创建的两个空数组,只有elementData
默认容量为10,由无参构造函数调用有参构造函数时指定。

    protected Object[] elementData;
    
    protected int elementCount;

    /**
     * The amount by which the capacity of the vector is automatically
     * incremented when its size becomes greater than its capacity.  If
     * the capacity increment is less than or equal to zero, the capacity
     * of the vector is doubled each time it needs to grow.
     *
     * @serial
     */
    protected int capacityIncrement;
    /**
     * Constructs an empty vector with the specified initial capacity and
     * capacity increment.
     */
    public Vector(int initialCapacity, int capacityIncrement) {...}
    
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

    /**
     * Constructs an empty vector so that its internal data array
     * has size {@code 10} and its standard capacity increment is
     * zero.
     */
    public Vector() {
        this(10);
    }
(2)同步

它的实现与 ArrayList 类似,但是在方法声明上使用了 synchronized 进行同步。

(3)扩容

Vector 的构造函数可以传入 capacityIncrement 参数,它的作用是在扩容时使容量 capacity 增长 capacityIncrement。如果这个参数的值<=0,扩容时每次都令 capacity 为原来的两倍。

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

4、LinkedList

(1)实现方式

JDK1.7后双向链表,并在LinkedList中维护头尾节点指针。

	transient int size = 0;

    /** Pointer to first node.*/
    transient Node<E> first;

    /** Pointer to last node.*/
    transient Node<E> last;

5、HashMap

(1)默认参数值

loadFactor 太大导致碰撞概率高,查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。

	// 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量
    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;
(2)put

JDK1.8对于链表的put采用尾插法,JDK1.7对于链表的put采用头插法

JDK1.8 put步骤如下:

  1. table未初始化或长度为0,将table进行扩容
  2. 通过(n - 1) & hash,计算要插入的元素在数组中的位置。若该位置没有元素,则插入该元素;
  3. 否则,比较该位置元素的hash值与key值是否跟要插入的元素相同。若相同,则进行覆盖;
  4. 若不相同,判断该节点是红黑树节点还是链表节点。若是红黑树节点,将该元素插入到树中;
  5. 若是链表节点,遍历链表节点,若其中有元素的hash值与key值跟要插入的元素相同,则对该位置进行覆盖;
  6. 若链表中没有相同的元素,则将该元素插到链表末尾,并同时判断此时这条链表中的元素数目。
  7. 若链表中元素的数目>=8:并且table的长度>=64,则该条链表转化为红黑树;否则进行扩容处理
  8. 对于直接覆盖的方式,方法进行元素值替换后,返回旧元素值
  9. 对于添加新节点的的方式,判断新增元素后是否需要扩容。方法返回null

HashMap 的长度为什么是 2 的幂次方?计算元素位置采用 (n - 1) & hash的原因:

  1. 为了让HashMap 存取高效,尽量较少碰撞,需要把数据均匀分配。
  2. Hash值范围过大(Integer),需要进行取余操作才能映射到数组中。
  3. 而取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。二进制位操作 &,相对于%能够提高运算效率。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

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,用e来记录
                e = p;
        // hash值不相等,即key不相等;为红黑树结点
        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);
                    // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
                    // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
                    // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
                    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;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) {
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}
(3)get

JDK1.8 get步骤:

  1. 数组未初始化或数组长度<=0return null
  2. 通过(n - 1) & hash,计算要插入的元素在数组中的位置
  3. 若该位置元素不为null,并且hash值与key值都相等,则返回该元素
  4. 否则,若该桶中不止一个节点,判断该位置元素的类型
  5. 若是红黑树节点,则在红黑树中进行查找;若是链表节点,则在链表中进行查找
  6. 若未找到,返回null
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) {
            // 在树中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在链表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
(4)扩容

JDK1.8扩容步骤:

  1. 获取原数组的长度oldCap,原始的扩容阈值oldThr,并分为三种情况进行处理:
    (1)若原数组的长度oldCap>0,与最大容量MAXIMUM_CAPACITY进行比较,
        (a)若oldCap>=MAXIMUM_CAPACITY,则阈值设为Integer.MAX_VALUEreturn
        (b)若小于,则将新数组的长度newCap变为原来的两倍,若oldCap >= DEFAULT_INITIAL_CAPACITY,则将新的扩容阈值newThr也变为原来的两倍
    (2)如果原数组的扩容阈值oldThr>0,则将新数组的长度newCap变为原数组的扩容阈值
    (3)若原数组的长度oldCap和扩容阈值oldThr都为0,则对其进行初始化
  2. 若此时,新数组的扩容阈值newThr还为0,则重新计算新数组的扩容阈值
  3. 将原数组中的元素放入新数组中,分为以下三种情况:
    (1)若元素是单个结点,e.hash & (newCap - 1)重新计算在新数组中的位置即可
    (2)若元素是红黑树类型,则采用红黑树中的方法计算在新数组中的位置
    (3)若元素是链表节点类型,采用(e.hash & oldCap)==0将链表分为两个,计算在新数组中位置newTab[j],newTab[j + oldCap]

采用newTab[j],newTab[j + oldCap]计算链表在新数组中位置的原因:

正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。

在这里插入图片描述
hash值的每个二进制位用abcde来表示,那么,hash和新旧table按位与的结果,最后4位显然是相同的,唯一可能出现的区别就在第5位,也就是hash值的b所在的那一位,如果b所在的那一位是0,那么新table按位与的结果和旧table的结果就相同,反之如果b所在的那一位是1,则新table按位与的结果就比旧table的结果多了10000(二进制),而这个二进制10000就是旧table的长度16。

原文链接:Hashmap实现原理及扩容机制详解

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 {
        // 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 {
                    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) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
(5)HashMap多线程存在的问题

HashMap的线程不安全主要体现在下面两个方面:

  1. 在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。(原因:头插法)
  2. 在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。

JDK1.7线程不安全的原因

HashMap线程不安全的表现 – Java 8

JDK1.7和JDK1.8中HashMap为什么是线程不安全的?

(6)HashMap JDK1.7与JDK1.8的不同点
  1. JDK1.7 底层数据结构:数组+链表(扩容:头插法)
  2. JDK1.8 底层数据结构:数组+链表(扩容:尾插法)+红黑树
  3. 元素插入流程:1.7中是先判断是否需要扩容,再插入。1.8中是先插入,插入成功之后再判断是否需要扩容;
  4. 扩容方式:1.7中需要对原数组中元素重新进行hash定位在新数组中的位置。1.8中采用更简单的逻辑判断,原下标位置或原下标+旧数组的大小。
(7)HashMap红黑树的阈值为什么是8?
  1. hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。
  2. Java的源码贡献者在进行大量实验发现,hash碰撞发生8次的概率已经低于百万分之一,几乎为不可能事件,如果真的碰撞发生了8次,那么说明hash函数选择的不好,后序可能还会继续发生hash碰撞。所以,这个时候,就应该将链表转换为红黑树了,也就是为什么链表转红黑树的阈值是8。
  3. 红黑树转链表的阈值为6,主要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会发生链表和红黑树的不停相互激荡转换,白白浪费资源。

详解:HashMap红黑树的阈值为什么是8?

(8)HashMap中的哈希函数时怎么实现的?

key的hashcode是一个32位的int类型值,hash函数就是将key.hashcode与key.hashcode高16位进行异或运算。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

哈希函数为什么这么设计?

这是一个扰动函数,这样设计的原因主要有两点:

  • 可以最大程度的降低hash碰撞的概率(hash值越分散越好);
  • 因为是高频操作,所以采用位运算,让算法更加高效;
(7)HashMap选择红黑树的原因

红黑树相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。
对于插入删除等操作效率提高很多。红黑树不像avl树一样追求绝对的平衡,他允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价较大,所以效率不如红黑树。

(8)红黑树相关概念

红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(Null)是黑色。 [注意:这里叶子节点,是指为空(NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

在这里插入图片描述

6、ConcurrentHashMap

(1)ConcurrentHashMap 和 Hashtable 的区别
  1. 底层数据结构:
    (1)HashTable:数组+链表
    (2)JDK1.7 ConcurrentHashMap:分段数组+链表
    (3)JDK1.8 ConcurrentHashMap:数组+链表/红黑树
  2. 实现线程安全的方式:
    (1)Hashtable :对所有的读写等操作都进行了锁(synchronized)保护,效率低下。(在方法上加锁)
    (2)JDK1.7 ConcurrentHashMap: 对整个桶数组进行了分割分段(Segment),每一把锁只锁其中一个Segment,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
    (3)JDK1.8 ConcurrentHashMap:并发控制使用 synchronized 和 CAS 来操作。
    (4)Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7、Collections.synchronizedList

8、Collections.synchronizedMap

9、Collections.synchronizedSet

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值