目录
题目 1:JVM 整体结构是什么样的? 8
题目 3:Object 类有哪些方法? 11
题目 4:静态变量与实例变量区别? 11
题目 5:String 类的常用方法有哪些? 11
题目 6:数组有没有 length()方法?String 有没有 length() 12
题目 7:String、StringBuffer、StringBuilder 的区别? 12
题目 8:String str = “i” 和 String str = new String(“1”)一样吗? 12
题目 9:String s=new String(“xyz”);创建了几个字符串对象? 13
题目 10:基本数据类型,int 初始值,integer 初始值 13
题目 11:字符串操作:如何实现字符串的反转及替换? 13
题目 12:Java 中怎样将 bytes 转换为 long 类型? 13
题目 13:float f=3.4;是否正确? 13
题目 14:a = a + b 与 a += b 的区别? 13
题目 15:short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 14
题目 16:Java 中应该使用什么数据类型来计算价格? 14
题目 17:==与 equals 的区别? 14
题目 18:接口和抽象类的区别是什么? 14
题目 19:Java 中的值传递和引用传递? 15
题目 20:sleep 和 wait 的区别? 16
题目 21:请写出你最常见的 几个 RuntimeException? 与非运行时异常的区别? 16
• 运行时异常 16
题目 22:Error 和 Exception 的区别? 17
题目 23:Java 反射有了解吗? 17
题目 24:Java 注解可以加在什么地方?Java 自带注解有哪些?哪里有用到注解? 17
题目 25:Java 中的 final 关键字有哪些用法? 18
题目 26:try catch 有 return,发生异常,走 return 还是 finally 18
题目 27:接口 1.8 后新特性 18
题目 28:浅拷贝和深拷贝区别 19
题目 30:&操作符和&&操作符有什么区别? 23
• 题目 1:集合类中主要有几种接口? 23
• 题目 2:集合中泛型常用特点和好处? 23
• 题目 3:List、Set、Queue、Map 的区别? 24
• 题目 4:集合类的底层数据结构? 24
• 题目 5:如何选用集合类? 25
• 题目 6:HashSet 如何检查重复? 26
• 题目 7:HashSet 和 TreeSet 区别? 26
• 题目 8:HashMap 和 HashSet 区别? 26
• 题目 9:HashMap 和 HashTable 区别? 26
• 题目 10:HashMap 和 TreeMap 区别? 28
• 题目 11:ConcurrentHashMap 和 HashTable 区别? 28
–实现线程安全的方式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap 29
• 题目 12:ArrayList 和 linkedList 区别? 29
• 题目 13:HashMap 底层实现原理? 30
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 30
• 题目 14:HashMap 什么时候扩容? 30
• 题目 15:HashMap 中的 key 我们可以使用任何类作为 31
• 题目 16:HashMap 的长度为什么是 2 的 N 次方呢? 31
• 题目 17:Collection 和 Collections 的区别? 32
• 题目 18:数组 (Array) 和列表 (ArrayList) 区别? 32
题目 19:BIO 和 NIO 区别? 32
题目 20:Java 中有几种类型的流? 32
按照流的方向: 32
按照处理数据的单位: 33
题目 21:字节流和字符流区别? 33
题目 22:字节流有了为什么还要有字符流? 33
题目 23:什么是 java 序列化,如何实现 java 序列化? 33
序列化: 33
序列化的实现: 34
• 题目 24:红黑树有什么特征? 34
题目 1:创建线程有几种方式? 35
题目 2:线程的状态转换? 36
题目 3:start 和 run 的区别? 38
题目 4:Java 中用到的线程调度算法是什么? 38
题目 5:为什么使用线程池,优势是什么? 39
题目 6:线程池工作原理? 39
题目 7:线程池重要参数有哪些? 40
题目 8:线程池如何使用? 40
题目 9:线程池中会用到哪些队列? 42
题目 10:线程池的拒绝策略有哪些? 43
题目 11:线程池状态有哪些? 44
题目 12:线程池被创建后里面有线程吗?如果没有的话,你知道有什么方法对线程池进行预热吗? 44
题目 13:核心线程和非核心线程的销毁机制? 45
题目 14:线程池内抛出异常,线程池会怎么办? 46
题目 15:submit 和 execute 方法的区别? 46
题目 16:shutdown 和 shutdownNow 的区别? 46
题目 17:线程池如何重用线程的? 47
题目 18:线程池大小如何设定? 47
• 如何判断是 CPU 密集任务还是 IO 密集任务? 47
题目 19:线程池在实际项目中的使用场景? 48
题目 20:项目中多个业务需要用到线程池,是为每个线程池都定义一个还是定义一个公共的线程池呢? 48
题目 21:说说 synchronized 的实现原理? 48
题目 22:Synchronized 作用范围? 48
题目 23:synchronized 和 volatile 的区别? 49
题目 24:synchronized 和 lock 的区别? 49
题目 25:Java 中有哪些锁? 51
Java 热门面试题-基础
题目 1:JVM 整体结构是什么样的?
题目 2:JVM 运行时数据区描述下?
运行时数据区域被划分为 5 个主要组件:
方法区(Method Area)
所有类级别数据将被存储在这里,包括静态变量。每个 JVM 只有一个方法区,它是一个共享的资源。
堆区(Heap Area)
所有的对象和它们相应的实例变量以及数组将被存储在这里。每个 JVM 同样只有一个堆区。由于方法区和堆区的内存由多个线程共享,所以存储的数据不是线程安全的。
栈区(Stack Area)
对每个线程会单独创建一个运行时栈。对每个函数呼叫会在栈内存生成一个栈帧 (Stack Frame)。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧被分为三个子实体:
Ø 局部变量数组 – 包含多少个与方法相关的局部变量并且相应的值将被存储在这里。
Ø 操作数栈 – 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令。
Ø 帧数据 – 方法的所有符号都保存在这里。在任意异常的情况下,catch 块的信息将会被保存在帧数据里面。
PC 寄存器
每个线程都有一个单独的 PC 寄存器来保存当前执行指令的地址,一旦该指令被执行,pc 寄存器会被更新至下条指令的地址。
本地方法栈
本地方法栈保存本地方法信息。对每一个线程,将创建一个单独的本地方法栈。
题目 3:Object 类有哪些方法?
Clone、equals、 hashcode、wait、notify、notifyall、finalize、toString、getClass
题目 4:静态变量与实例变量区别?
•静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的 加载过程中,JVM 只为静态变量分配一次内存空间。
•实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象 的,在内存中,创建几次对象,就有几份成员变量。
题目 5:String 类的常用方法有哪些?
•indexof(); 返回指定字符的的索引。
•charAt(); 返回指定索引处的字符。
•replace(); 字符串替换。
•trim(); 去除字符串两端空格。
•split();字符串分割,返回分割后的字符串数组。
•getBytes();返回字符串 byte 类型数组。
•length();返回字符串长度。
•toLowerCase(); 将字符串转换为小写字母。 •toUpperCase();将字符串转换为大写字母。
•substring(); 字符串截取。
•equals(); 比较字符串是否相等。
题目 6:数组有没有 length()方法?String 有没有 length()
方法?
数组没有 length()方法,有 length 的属性。String 有 length()方法。JavaScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆
题目 7:String、StringBuffer、StringBuilder 的区别?
•第一点: 可变和适用范围。String 对象是不可变的,而 StringBuffer 和StringBuilder是可变字符序列。每次对 String 的操作相当于生成一个新的 String 对象,而对 StringBuffer 和 StringBuilder 的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用 String,因为频繁的生成对象将会对系统性能产生影响。
•第二点: 线程安全。String 由于有 final 修饰,是 immutable 的,安全性是简单而纯粹的。StringBuilder 和 StringBuffer 的区别在于 StringBuilder 不保证同步,也就是说如果需要线程安全需要使用 StringBuffer,不需要同步的 StringBuilder 效率更高。
•总结:
–操作少量的数据 = String
–单线程操作字符串缓冲区下操作大量数据 = StringBuilder
–多线程操作字符串缓冲区下操作大量数据 = StringBuffer
题目 8:String str = “i” 和 String str = new String(“1”)一样吗?
不一样,因为内存的分配方式不一样。String str = “i”的方式 JVM 会将其分配到常量池中,此时仅产生一个字符串对象。 String str = new String(“i”),JVM 会先在堆内存分配一个 String 对象,然后该对象指向常量池的字符串常量对象,如果字符串之前不存在,相当于创建了 2 个对象。
题目 9:String s=new String(“xyz”);创建了几个字符串对象?
两个对象,一个是静态存储区的“xyz”,一个是用 new 创建在堆上的对象。
题目 10:基本数据类型,int 初始值,integer 初始值
8 中基本数据类型,int 初始 0,integer 初始 null
题目 11:字符串操作:如何实现字符串的反转及替换?
可用字符串构造 StringBuffer 对象,然后调用 StringBuffer 中的 reverse 方法即可实现字符串的反转,调用 replace 方法即可实现字符串的替换。
题目 12:Java 中怎样将 bytes 转换为 long 类型?
String 接收 bytes 的构造器转成 String,再 Long.parseLong
题目 13:float f=3.4;是否正确?
不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型
(downcasting,也称为窄化)会造成精度损失,因此需要强制类型转换 float f
=(float)3.4; 或者写 成 float f =3.4F;。
题目 14:a = a + b 与 a += b 的区别?
+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两这个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。
byte a = 127; byte b = 127;
b = a + b; // 错误 : cannot convert from int to byte
b += a; // 争取
(因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错)
题目 15:short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1
+= 1;有错吗?
对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。而 short s1 = 1; s1+= 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
题目 16:Java 中应该使用什么数据类型来计算价格?
如果不是特别关心内存和性能的话,使用BigDecimal,否则使用预定义精度的 double
类型
题目 17:==与 equals 的区别?
区别 1. ==是一个运算符 equals 是 Object 类的方法区别 2. 比较时的区别
•用于基本类型的变量比较时: ==用于比较值是否相等,equals 不能直接用于基本数据类型的比较,需要转换为其对应的包装类型。
•用于引用类型的比较时。==和 equals 都是比较栈内存中的地址是否相等 。相等为 true 否则为 false。但是通常会重写 equals 方法去实现对象内容的比较。
题目 18:接口和抽象类的区别是什么?
•抽象类可以提供成员方法的实现细节,而接口中只能存在 public abstract 方法;
•抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
–接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
•一个类只能继承一个抽象类,而一个类却可以实现多个接口。
题目 19:Java 中的值传递和引用传递?
•值传递
在方法的调用过程中,实参把它的实际值传递给形参,此传递过程就是将实参的值复制
一份传递到函数中,这样如果在函数中对该值(形参的值)进行了操作将不会影响实参
的值。 因为是直接复制,所以这种方式在传递大量数据时,运行效率会特别低下。
•引用传递
引用传递弥补了值传递的不足,如果传递的数据量很大,直接复过去的话,会占用大量
的内存空间,而引用传递就是将对象的地址值传递过去,函数接收的是原始值的首地址
值。
在方法的执行过程中,形参和实参的内容相同,指向同一块内存地址,也就是说操作的
其实都是源数据,所以方法的执行将会影响到实际对象结论:
基本数据类型传值,对形参的修改不会影响实参;
引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改
会影响到实际的对象。String, Integer, Double 等 immutable 的类型特殊处理,可以
理解为传值,最后的操作不会修改实参对象
题目 20:sleep 和 wait 的区别?
•sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。
•wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
题目 21:请写出你最常见的 几个 RuntimeException? 与非运行时异常的区别?
•java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在
的对象。
•java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数
字型字符。
•java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
•java.lang.NoSuchMethodException 方法不存在异常。
•java.lang.ClassCastException 数据类型转换异常。
注意:IOException、SQLException、ClassNotFoundException 不是运行时异常
•运行时异常
都是 RuntimeException 类及其子类异常,如 NullPointerException(空指针异常)、 IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是 Java 编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用 try-catch 语句捕获它,也没有用 throws 子句声明抛出它,也会编译通过。
•非运行时异常 (编译异常)
是 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、 SQLException 等以及用户自定义的 Exception 异常,一般情况下不自定义检查异常。
题目 22:Error 和 Exception 的区别?
•Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困 难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;
•Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。
题目 23:Java 反射有了解吗?
在 Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为 Java 语言的反射机制。通过反射机制使我们所写的代码更具有「通用性」和「灵活性」,比如 Spring/Spring Boot、MyBatis等框架大量用到了反射机制。比如类上加上@Component 注解,Spring 就帮你创建对象,比如约定大于配置。
题目 24:Java 注解可以加在什么地方?Java 自带注解有哪些?哪里有用到注解?
•注解用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解
•** Java 自带的标准注解**,包括@Override、@Deprecated 和@SuppressWarnings,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。
•注解应用场景:Spring、SpringMVC 中大量注解、单元测试注解
题目 25:Java 中的 final 关键字有哪些用法?
•修饰类:表示该类不能被继承;
•修饰方法:表示方法不能被重写;
•修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。
题目 26:try catch 有 return,发生异常,走 return 还是 finally
1.不管有没有异常,finally 块中代码都会执行;
2.当 try.catch 中有 return 时,finally 仍然会执行;
3.finally 中最好不要包含 return,否则程序会提前退出,返回值不是 try 或 catch
中保存的返回值。
4.在执行时,是 return 语句先把返回值写入内存中,然后停下来等待 finally 语句块执行完,return 再执行后面的一段。
5.至于返回值到底变不变,当 finally 调用任何可变的 API,会修改返回值;当 finally
调用任何的不可变的 API,对返回值没有影响。
题目 27:接口 1.8 后新特性
java1.8 以后,接口中可定义默认(default)和静态方法(static),这两种方法都可以有具体实现,
实现该接口的类也可继承这两种方法去直接使用,也可对其进行重写
默认方法存在的两大优势:
1.可以让接口更优雅的升级,减少使用人员操作的负担
不必随着接口方法增加,从而修改实现代码,因为默认方法在子类中可以不用实
现
2.可以让实现类中省略很多不必要方法的空实现
题目 28:浅拷贝和深拷贝区别
浅: 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。
深:深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍
题目 29:Java 内存泄漏
内存泄露是: 尽管对象不再被程序所使用,但垃圾回收器却无法将其回收的情况——因为对象仍然处于被引用的状态。 久而久之,不能被回收的内存越来越多,最终导致内存溢出 OOM(OutOfMemoryError)。
1.内存泄漏发生的重灾区——堆。因为堆区是用来存储新生的 Java 对象的地方,这里也常会有不被使用的对象未被回收。为堆设置更小的内存是解决堆区内存泄漏的常用方法。在我们启动程序的时候,便可以调整我们需要的内存空间:
-Xms(初始化堆内存) –Xmx(最大堆内存)
2.静态类型的对象的引用也会导致 Java 内存泄漏。
它在全生命周期内都不会被 JVM 回收。格外注意对关键词 static 的使用,对任何集合或者是庞大的类进行 static 声明都会使其声明周期与 JVM 的生命周期同步,从而使其无法回收。
3.未关闭的流
忘记关闭流也是一种导致内存泄漏发生的常见情况。一个未能关闭的流会导致两种类型的泄漏,一种是低层资源泄漏,一种是内存泄漏。低层资源泄漏是 OS 层面的资源,例如文件描述符,打开连接等的泄漏。JVM 会跟踪记录这些重要的资源,进一步也就导致了内存泄漏。
4.未关闭的连接
对未关闭的连接的处理(例如数据库,FTP 服务器等)。同样,错误的实现方法也可能导致内存泄漏。使用完连接后要及时关闭。
题目 30:什么是双亲委派模型?为什么要使用双亲委派模型?
类加载器,顾名思义就是一个可以将 Java 字节码加载为 java.lang.Class 实例的工具。这个过程包括,读取字节数组、验证、解析、初始化等。另外,它也可以加载资源,包括图像文件和配置文件
类加载器的特点:
动态加载,无需在程序一开始运行的时候加载,而是在程序运行的过程中,动态按需加载,字节码的来源也很多,压缩包 jar、war 中,网络中,本地文件等。类加载器动态加载的特点为热部署,热加载做了有力支持。
全盘负责,当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类都由这个类加载器加载,除非在程序中显式地指定另外一个类加载器加载。所以破坏双亲委派不能破坏扩展类加载器以上的顺序。
类加载器可以分为两种:一种是启动类加载器,由 C++语言实现,是虚拟机自身的一部分;另一种是继承于 java.lang.ClassLoader 的类加载器,包括扩展类加载器、应用程序类加载器以及自定义类加载器。
双亲委派:
为什么双亲委派:
双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。
一个非常明显的目的就是保证 java 官方的类库
例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。
如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。
题目 30:&操作符和&&操作符有什么区别?
答案:当一个&表达式在求值的时候,两个操作数都会被求值,&&更像是一个操作符的快捷方式。当一个&&表达式求值的时候,先计算第一个操作数,如果它返回 true才会计算第二个操作数。如果第一个操作数取值为 fale,第二个操作数就不会被求值。
Java 热门面试题-集合与 IO
•题目 1:集合类中主要有几种接口?
–Collection: Collection 是集合 List、 Set、 Queue 的最基本的接口。
–Iterator:迭代器,可以通过迭代器遍历集合中的数据
–Map:是映射表的基础接口
•题目 2:集合中泛型常用特点和好处?
《Java 核心技术》中对泛型的定义是:
“泛型” 意味着编写的代码可以被不同类型的对象所重用。
“泛型”,顾名思义,“泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的。ArrayList 就是个泛型类, ArrayList 作为集合可以存放各种元素,如 Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的
规则来约束,如我们可以约束集合中只存放 Integer 类型的元素,如使用泛型的好处?
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集
合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而
这并不是最重要的,因为我们只要把底层存储设置了 Object 即可,添加的数据全部都可向上转型为
Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
•题目 3:List、Set、Queue、Map 的区别?
–List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
–Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
–Queue(实现排队功能的“叫号机”): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
–Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
•题目 4:集合类的底层数据结构?
–List
–Set
•Arraylist: Object[] 数组
•Vector:Object[] 数组
•LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
•HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap
来保存元素
•LinkedHashSet: LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
•TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树) –Queue
•PriorityQueue: Object[] 数组来实现二叉堆
•ArrayQueue: Object[] 数组 + 双指针再来看看 Map 接口下面的集合。
–Map
•HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时