Java基础
- 1、⾯向对象和⾯向过程的区别
- 2、关于JVM、JDK和JRE的详解
- 3、Oracle JDK 和 OpenJDK 的对⽐
- 4、Java 和 C++的区别?
- 5、字符型常量和字符串常量的区别?
- 6、重载和重写的区别?
- 7、Java ⾯向对象编程三⼤特性
- 8、String StringBuffer 和 StringBuilder 的区别是什么?
- 9、⾃动装箱与拆箱
- 10、在 Java 中定义⼀个不做事且没有参数的构造⽅法的作⽤
- 11、接⼝和抽象类的区别是什么?
- 12、成员变量与局部变量的区别有哪些?
- 13、简述线程、程序、进程的基本概念。以及他们之间关系是什么?
- 14、Java 中的 IO 流
- 15、Java集合
-
- 15.1、List、Set、Map三者的区别?
- 15.2、Arraylist 与 LinkedList 区别?
- 15.3、双向链表和双向循环链表的区别
- 15.4、ArrayList 与 Vector 的区别? 为什么要⽤Arraylist取代Vector呢?
- 15.5、HashMap 和 Hashtable 的区别
- 15.6、HashMap 和 HashSet区别
- 15.7、HashSet如何检查重复
- 15.8、HashMap的底层实现
- 15.9、HashMap 的⻓度为什么是2的幂次⽅
- 15.10、HashMap 多线程操作导致死循环问题
- 15.11、HashMap 多线程操作导致死循环问题
- 15.12、ConcurrentHashMap线程安全的具体实现⽅式/底层具体实现
- 15.13、⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
- 15.14、集合框架底层数据结构总结
- 15.15、如何选⽤集合?
- 16、多线程
1、⾯向对象和⾯向过程的区别
-
⾯向过程 :⾯向过程性能⽐⾯向对象⾼。 因为类调⽤时需要实例化,开销⽐较⼤,⽐较消
耗资源,所以当性能是最重要的考量因素的时候,⽐如单⽚机、嵌⼊式开发、Linux/Unix 等⼀般采⽤⾯向过程开发。但是,⾯向过程没有⾯向对象易维护、易复⽤、易扩展。 -
⾯向对象 :⾯向对象易维护、易复⽤、易扩展。 因为⾯向对象有封装、继承、多态性的特
性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,⾯向对象性能⽐⾯向过程低。 -
为什么说面向过程比面向对象效率高?
其实面向过程也需要分配内存空间,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是因为Java是半编译语言(编译+解释共存的语言),最终执行的代码并不是可以直接被CPU执行的二级制机器码。
而面向过程语言大多是直接编译成机器码在CPU上运行的,并且其他一些面向过程的脚本语言性能也并不一定比Java好。
2、关于JVM、JDK和JRE的详解
2.1、JVM
Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM 有针对不同系统的特定实现
(Windows,Linux,macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果,这就是Java语言跨平台的原因。
2.2、什么是字节码?采⽤字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做 字节码
(即扩展名为 .class
的⽂件),它不⾯
向任何特定的处理器,只⾯向虚拟机。Java 语⾔通过字节码的⽅式,在⼀定程度上解决了
传统解释型语⾔执行效率低的问题,同时⼜保留了解释型语⾔可移植
的特点。所以 Java 程
序运⾏时⽐较⾼效,⽽且,由于字节码并不针对⼀种特定的机器,因此,Java 程序⽆须重
新编译便可在多种不同操作系统的计算机上运⾏。
2.3、Java 程序从源代码到运⾏步骤
我们需要格外注意的是 .class->机器码
这⼀步。在这⼀步 JVM 类加载器⾸先加载字节码⽂件,然后通过解释器
逐⾏解释执⾏机器码
,这种⽅式的执⾏速度会相对⽐较慢。⽽且,有些⽅法和代码块是经常需要被调⽤的(也就是所谓的热点代码SpotHot
),所以后⾯引进了 JIT 编译器
,⽽ JIT 属于运⾏时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使⽤。⽽我们知道,机器码的运⾏效率肯定是⾼于 Java 解释器的。这也解释了我们为什么经常会说Java 是编译与解释共存的语⾔
。
小贴士:
HotSpot 采⽤了惰性评估(Lazy Evaluation)的做法,根据⼆⼋定律,消耗⼤部分系统资源的
只有那⼀⼩部分的代码(热点代码),⽽这也就是 JIT 所需要编译的部分。JVM 会根据代
码每次被执⾏的情况收集信息并相应地做出⼀些优化,因此执⾏的次数越多,它的速度就越
快。JDK 9 引⼊了⼀种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码
编译成机器码,这样就避免了 JIT 预热等各⽅⾯的开销。JDK ⽀持分层编译和 AOT 协作使
⽤。但是 ,AOT 编译器的编译质量是肯定⽐不上 JIT 编译器的。
总结:
Java虚拟机(JVM)是运行Java字节码的虚拟机。JVM可以针对不同系统的特定实现(Windows、Linux、macOS)不同操作系统,目的是使用相同的字节码,他们都可以给出相同的结果。字节码和不同系统的JVM实现的是Java的一次编译,随处可以执行
,这就是Java语言跨平台的原因。
2.4、JDK 和 JRE的区别
-
JDK是Java Development Kit,它是功能齐全的Java SDK,为Java语言提供了开发环境和运行环境。它拥有JRE所拥有的一切,还有编译器
javac
和工具javadoc和jdb
,它能够创建和编译程序。 -
JRE是Java运行时环境。它是运行已编译Java程序所需要的所有内容的集合,包括Java虚拟机(JVM),Java类库,Java命令和其他的一些基础构件。但是,它不能创建新的Java程序。
-
如果你只是为了运⾏⼀下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进⾏⼀些 Java 编程⽅⾯的⼯作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进⾏任何 Java 开发,仍然需要安装 JDK。例如,如果要使⽤ JSP 部署 Web 应⽤程序,那么从技术上讲,您只是在应⽤程序服务器中运⾏ Java 程序。那你为什么需要 JDK 呢?因为应⽤程序服务器会将 JSP 转换为 Java servlet,并且需要使⽤ JDK 来编译 servlet。
3、Oracle JDK 和 OpenJDK 的对⽐
对于 Java 7,没什么关键的地⽅。OpenJDK 项⽬主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle ⼯程师维护。
3.1、OpenJDK 存储库中的源代码与⽤于构建 Oracle JDK 的代码之间有什么区别?
⾮常接近 ,Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了⼏个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及⼀些封闭的源代码派对组件,如图形光栅化器,⼀些开源的第三⽅组件,如 Rhino,以及⼀些零碎的东⻄,如附加⽂档或第三⽅字体。展望未来,我们的⽬的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分。
3.2、总结区别
- OpenJDK 是⼀个参考模型并且是完全开源的,⽽ Oracle JDK 是 OpenJDK 的⼀个实现,并不是完全开源的;
- Oracle JDK ⽐ OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码⼏乎相同,但 Oracle
JDK 有更多的类和⼀些错误修复。因此,如果您想开发企业/商业软件,我建议您选择
Oracle JDK,为它经过了彻底的测试和稳定。某些情况下,有些⼈提到在使⽤ OpenJDK
可能会遇到了许多应⽤程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题; - 在响应性和 JVM 性能⽅⾯,Oracle JDK 与 OpenJDK 相⽐提供了更好的性能;
- Oracle JDK 不会为即将发布的版本提供⻓期⽀持,⽤户每次都必须通过更新到最新版本获
得⽀持来获取最新版本; - Oracle JDK 根据⼆进制代码许可协议获得许可,⽽ OpenJDK 根据 GPL v2 许可获得许可。
4、Java 和 C++的区别?
- 是⾯向对象的语⾔,都⽀持封装、继承和多态
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ ⽀持多重继承;虽然 Java 的类不可以多继承,但是接⼝可以多继承。Java 有⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存。
- 在 C 语⾔中,字符串或字符数组最后都会有⼀个额外的字符‘\0’来表示结束。但是,Java 语⾔中没有结束符这⼀概念。
5、字符型常量和字符串常量的区别?
- 形式上: 字符常量是单引号引起的⼀个字符; 字符串常量是双引号引起的若⼲个字符
- 含义上: 字符常量相当于⼀个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表⼀个地址值(该字符串在内存中存放位置)
- 占内存⼤⼩:字符常量只占 2 个字节; 字符串常量占若⼲个字节 (注意: char 在 Java 中占两个字节)
6、重载和重写的区别?
- 重载就是同样的⼀个⽅法能够根据输⼊数据的不同,做出不同的处理;
- 重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法。
6.1、重载
重载发生在编译器,并且在同⼀个类中,⽅法名必须相同,参数类型不同、个数不同、顺序不同,⽅法返回值和访问修饰符可以不同。
综上:重载就是同⼀个类中多个同名⽅法根据不同的传参来执⾏不同的逻辑处理。
6.2、重写
重写发⽣在运⾏期,是⼦类对⽗类的允许访问的⽅法的实现过程进⾏重新编写。
- 返回值类型、⽅法名、参数列表必须相同,抛出的异常范围⼩于等于⽗类,访问修饰符范围⼤于等于⽗类。
- 如果⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰的⽅法能够被再次声明。
- 构造⽅法⽆法被重写。
综上:重写就是⼦类对⽗类⽅法的重新改造,外部样⼦不能改变,内部逻辑可以改变。
6.3、区别对照
区别点 | 重载 | 重写 |
---|---|---|
参数列表 | 必须修改 | 一定不能修改 |
返回值类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法抛出异常应比父类方法声明抛出的异常类更小或想等 |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
7、Java ⾯向对象编程三⼤特性
7.1、封装
封装把⼀个对象的属性私有化,同时提供⼀些可以被外界访问的属性的⽅法,如果属性不想被外界访问,我们⼤可不必提供⽅法给外界访问。但是如果⼀个类没有提供给外界访问的⽅法,那么这个类也没有什么意义了。
7.2、继承
继承是使⽤已存在的类的定义作为基础建⽴新类的技术,新类的定义可以增加新的数据或新的功能,也可以⽤⽗类的功能,但不能选择性地继承⽗类。通过使⽤继承我们能够⾮常⽅便地复⽤以前的代码。
关于继承三要素:
- ⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是
只是拥有
⽗类中的私有属性和⽅法⼦类,却⽆法访问
。 - ⼦类可以拥有⾃⼰属性和⽅法,即⼦类可以对⽗类进⾏扩展。
- ⼦类可以⽤⾃⼰的⽅式实现⽗类的⽅法。
7.3、多态
所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
在 Java 中有两种形式可以实现多态:
- 继承(多个⼦类对同⼀⽅法的重写)
- 接⼝(实现接⼝并覆盖接⼝中同⼀⽅法)
8、String StringBuffer 和 StringBuilder 的区别是什么?
String 类中使⽤ final 关键字修饰字符数组来保存字符串, private final char value[]
,所以 String 对象是不可变的。
扩展:在 Java 9 之后,String 类的实现改⽤ byte 数组存储字符串
private final byte[] value
⽽ StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[] value 但是没有⽤ final 关键字修饰,所以这两种对象都是可变的。
8.1、线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是
StringBuilder 与 StringBuffer 的公共⽗类,定义了⼀些字符串的基本操作,如 pandCapacity、
append、insert、indexOf 等公共⽅法。StringBuffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁,所以是线程安全的。StringBuilder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。
8.2、性能
每次对 String 类型进⾏改变的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String对象。StringBuffer 每次都会对 StringBuffer 对象本身进⾏操作,⽽不是⽣成新的对象并改变对象引⽤。相同情况下使⽤ StringBuilder 相⽐使⽤ StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的⻛险。
8.3、总结
- 操作少量的数据: 适⽤
String
- 单线程操作字符串缓冲区下操作⼤量数据: 适⽤
StringBuilder
- 多线程操作字符串缓冲区下操作⼤量数据: 适⽤
StringBuffer
9、⾃动装箱与拆箱
- 装箱:将基本类型⽤它们对应的引⽤类型包装起来;
- 拆箱:将包装类型转换为基本数据类型。
10、在 Java 中定义⼀个不做事且没有参数的构造⽅法的作⽤
Java 程序在执⾏⼦类的构造⽅法之前,如果没有⽤ super() 来调⽤⽗类特定的构造⽅法,则会调⽤⽗类中“没有参数的构造⽅法”。因此,如果⽗类中只定义了有参数的构造⽅法,⽽在⼦类的构造⽅法中⼜没有⽤ super() 来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为 Java 程序在⽗类中找不到没有参数的构造⽅法可供执⾏。解决办法是在⽗类⾥加上⼀个不做事且没有参数的构造⽅法。
11、接⼝和抽象类的区别是什么?
- 接口的方法默认是
ppublic
,所有方法在接口中不能有实现(Java8开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 - 接口中除了
static、final
变量,不能有其他变量,而抽象类中则不一定。 - 一个类可以实现多个接口,但只能实现一个抽象类。接口本身可以通过
extends
关键字扩展多个接口。 - 接口方法默认修饰符是
public
,抽象方法可以有public、protected和default
这些修饰符(抽象方法就是为了被重写所以不能使用private
关键字修饰)。 - 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口时对行为的抽象,是一种行为的规范。
11.1、总结⼀下 JDK1.7~JDK1.9 中接⼝概念的变化
- 在 Java8 中,接⼝也可以定义静态⽅法,可以直接⽤接⼝名调⽤。实现类和实现是不
可以调⽤的。如果一个类同时实现两个接⼝,两个接⼝中定义了⼀样的默认⽅法,则必须重写,不然会报错。 - Java9中的接⼝允许定义私有⽅法 。
- 在 Java7 或更早版本中,接⼝⾥⾯只能有常量变量和抽象⽅法。这些接⼝⽅法必须由选择实现接⼝的类实现。
- Java8 的时候接⼝可以有默认⽅法和静态⽅法功能。
- Java9 在接⼝中引⼊了私有⽅法和私有静态⽅法。
12、成员变量与局部变量的区别有哪些?
- 从语法形式上看:成员变量是属于类的,⽽局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 从变量在内存中的存储方式来看:如果成员变量是使用
static
修饰的,name这个成员变量是属于类的;如果没有使用static
修饰,这个成员变量是属于实例的。对象存于堆内存,如果局变量类型为基本数据类型,那么存储在栈内存;如果为引用数据类型,那存放的是指向堆内存对象的引用或者是指向常量池中的地址。 - 从变量在内存中的生存时间来看:成员变量是对象的一部分,它随着对象的创建而存在;而局部变量随着方法的调用而自动消失。
- 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量必须显式地赋值),而局部变量则不会自动赋值。
13、简述线程、程序、进程的基本概念。以及他们之间关系是什么?
13.1、线程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个进程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小的多,也正为如此,线程也正被称为轻量级进程。
13.2、程序
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
13.3、进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单的来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间、内存空间,文件,输入输出设备的使用权等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一个程序内几乎同时执行一个以上的程序段。
13.4、线程有哪些基本状态?
Java 线程在运⾏的⽣命周期中的指定时刻只可能处于下⾯ 6 种不同状态的其中⼀个状态:
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程在⽣命周期中并不是固定处于某⼀个状态⽽是随着代码的执⾏在不同状态之间切换。Java 线程状态变迁如下图所示:
线程创建之后它将处于NEW(新建)
状态,调用start()
方法后开始运行,线程这时候处于READY(可与性)
状态。可运行状态的线程获得了CPU时间片(timeslice)后就处于RUNNING(运行)
状态。
操作系统隐藏
了Java 虚拟机(JVM)中的 READY
和RUNNING
状态,它只能看到RUNNABLE
状态,所以 Java 系统⼀般将这两个状态统称为 RUNNABLE(运⾏中) 状态 。
当线程执行wait()
方法之后,线程进入WAITING(等待)
状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而TIME_WAITING(超时等待)
,状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long mills)
方法或wait(long mills)
方法可以将Java线程置于TIME_WAITING
状态。当超时时间到达后Java线程将会返回到RUNNABLE
状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到BLOCKED(阻塞)
状态。线程在执行RUNNABLE的run()
方法之后将会进入到TERMINATED(终止)
状态。
14、Java 中的 IO 流
14.1、Java 中 IO 流分为⼏种?
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元分,可以划分为字节流和字符流;
- 按照流的角色分,可以分为节点流和处理流。
Java中的IO流共涉及到40多个类,这些类看上去比较复杂,但实际上很有原则,而且彼此之间存在非常紧密的联系,Java的IO流中都是从如下4个抽象类基类中派生出来的。
- InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流;
- OutputSteam/Writer:所有输出流的基类,前者是字节数出流,后者是字符输出流。
14.2、既然有了字节流,为什么还要有字符流?
问题本质:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么I/O流操作要分为字节流操作和字符流操作呢?
字符流是Java虚拟机将字节转换得到的,问题就出在这个过程还是比较非常耗时的,并且,如果我们不知道编码类型就很容易出现乱码问题。所以,I/O流就干脆直接提供了一个直接操作字符的接口,方便我们平时对字符进行操作。如果操作的是音频、图片、视频等媒体文件使用字节流比较合适,如果涉及到的是纯文本字符文件使用字符流比较好。
14.3、BIO,NIO,AIO 有什么区别?
- BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完
成。在活动连接数不是特别⾼(⼩于单机 1000)的情况下,这种模型是⽐较不错的,可以
让每⼀个连接专注于⾃⼰的 I/O 并且编程模型简单,也不⽤过多考虑系统的过载、限流等问题。线程池本身就是⼀个天然的漏⽃,可以缓冲⼀些系统处理不了的连接或请求。但是,当⾯对⼗万甚⾄百万级连接的时候,传统的 BIO 模型是⽆能为⼒的。因此,我们需要⼀种更⾼效的 I/O 处理模型来应对更⾼的并发量。 - NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型,在 Java 1.4 中引⼊了
NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它⽀持⾯向缓冲的,基于通道的 I/O 操作⽅法。NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和
ServerSocketChannel 两种不同的套接字通道实现,两种通道都⽀持阻塞和⾮阻塞两种模式。阻塞模式使⽤就像传统中的⽀持⼀样,⽐较简单,但是性能和可靠性都不好;⾮阻塞模式正好与之相反。对于低负载、低并发的应⽤程序,可以使⽤同步阻塞 I/O 来提升开发速率和更好的维护性;对于⾼负载、⾼并发的(⽹络)应⽤,应使⽤ NIO 的⾮阻塞模式来开发 - AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是异步⾮阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应⽤操作之后会直接返回,不会堵塞在那⾥,当后台处理完成,操作系统会通知相应的线程进⾏后续的操作。IO 是异步 IO 的缩写,虽然 NIO 在⽹络操作中,提供了⾮阻塞的⽅法,但是 NIO 的 IO ⾏为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程⾃⾏进⾏ IO 操作,IO 操作本身是同步的。查阅⽹上相关资料,我发现就⽬前来说 AIO 的应⽤还不是很⼴泛,Netty 之前也尝试使⽤过 AIO,不过⼜放弃了。
15、Java集合
15.1、List、Set、Map三者的区别?
- List集合:存储的元素是有序的、可重复的;
- Set集合:存储的元素是无序的、不重复的;
- Map集合:使用(kye-value)键值对存储,类似于数学上的函数y=f(x),x代表key,y代表value,key是无序的、不可重复的,value是无序的可重复的,每个key键最多映射到一个值。
15.2、Arraylist 与 LinkedList 区别?
-
是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是
非线程安全
; -
底层数据结构:
ArrayList
底层使用的是Object数组
;而LinkedList
底层使用的是双向链表
数据结构(JDK1.6之前为双向循环链表,JDK1.7取消了循环,注意双向链表和双向循环链表) -
插⼊和删除是否受元素位置的影响:
- ArrayList 采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响。
- LinkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置 i 插⼊和删除元素的话( (add(int index, Eelement) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。
-
是否⽀持快速随机访问:
LinkedList 不⽀持
⾼效的随机元素访问,⽽ArrayList ⽀持
。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法) -
内存空间占⽤:ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空
间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
15.3、双向链表和双向循环链表的区别
双向链表: 包含两个指针,⼀个 prev 指向前⼀个节点,⼀个 next 指向后⼀个节点。
双向循环链表: 最后⼀个节点的 next 指向 head,⽽ head 的 prev 指向最后⼀个节点,构成⼀个环。
15.4、ArrayList 与 Vector 的区别? 为什么要⽤Arraylist取代Vector呢?
ArrayList
是List
的主要实现类,底层使用的是Object[]
存储,适用于频繁的查找工作,线程不安全。Vector
是List
的古老实现类,底层使用Object[]
存储,线程安全的。
15.5、HashMap 和 Hashtable 的区别
- 底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较⼤的变化,当链表⻓度
⼤于阈值(默认为 8)(有一个前提将链表转换成红⿊树前会先判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。 - 线程是否安全: HashMap 是⾮线程安全的, HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ConcurrentHashMap,因为相比较效率比HashTable高,而且HashTable基本被淘汰了);
- 效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。另外, HashTable
基本被淘汰,不要在代码中使⽤它; - 对key和value是否支持null值:HashMap 可以存储 null 的 key 和 value,但 null 作为
键key只能有⼀个,null 作为值value可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。 - 初始容量⼤⼩和每次扩充容量⼤⼩的不同:
- 创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。
- 创建时如果给定了容量初始值,那么Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩( HashMap 中的 tableSizeFor() ⽅法保证的)。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩,后⾯会介绍到为什么是 2 的幂次⽅。
/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
15.6、HashMap 和 HashSet区别
源码中HashSet 底层就是基于 HashMap
实现的。HashSet 的源码⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。
HashMap | HashSet |
---|---|
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存储对象(包装类类型、引用) |
调用put() 方法向Map中添加元素 |
调用add() 方法向Set中添加元素 |
HashMap使用键key计算hashcode值 | HashSet使用成员对象来计算hashCode值,对于两个对象来说hashCode值可能相同,所以使用equals()方法来判断对象的相等性 |
15.7、HashSet如何检查重复
当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同时也会与其他加⼊的对象的 hashcode 值作⽐较,如果没有相符的 hashcode , HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让加⼊操作成功。
hashCode() 与 equals() 的相关规定:
- 如果两个对象相等,则 hashcode ⼀定也是相同的
- 两个对象相等,对两个 equals() ⽅法返回 true
- 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
- 综上, equals() ⽅法被覆盖过,则 hashCode() ⽅法也必须被覆盖
- hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该
class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)。
==与 equals 的区别:
-
对于基本类型来说 ⽐较的是值是否相等;
-
对于引⽤类型来说,⽐较的是两个引⽤是否指向同⼀个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同⼀个地⽅);
-
对于引⽤类型(包括包装类型)来说,equals 如果没有被重写,对⽐它们的地址是否相等;如果equals()⽅法被重写(例如 String),则⽐较的是地址⾥的内容。
15.8、HashMap的底层实现
详情这篇文章:https://blog.csdn.net/qq_52596258/article/details/120471348
JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列。HashMap 通过
key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n – 1) & hash 判断当前元素
存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash ⽅法。使⽤ hash ⽅法也就是扰动函数是为了防⽌⼀些实现⽐较差的 hashCode() ⽅法 换句话说使⽤扰动函数之后可以减少碰撞。
15.9、HashMap 的⻓度为什么是2的幂次⽅
为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上⾯也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n – 1) & hash ”。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次⽅。
这个算法应该如何设计呢?
我们⾸先可能会想到采⽤%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减⼀的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是2的幂次⽅。
15.10、HashMap 多线程操作导致死循环问题
主要原因在于 并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。
15.11、HashMap 多线程操作导致死循环问题
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的⽅式上不同
。
- 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现,DK1.8采⽤的数据结构跟 HashMap1.8 的结构⼀样,数组+链表/红⿊⼆叉树。 Hashtable 和JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
- 实现线程安全的⽅式(重要):
- ① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁)
对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本; - ② Hashtable (同⼀把锁) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
- ① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁)
两者的对⽐图:
-
JDK1.8 的 ConcurrentHashMap:
JDK1.8 的 ConcurrentHashMap 不在是 Segment 数组 + HashEntry 数组 + 链表,⽽是 Node 数组 + 链表 / 红⿊树。不过,Node 只能⽤于链表的情况,红⿊树的情况需要使⽤ TreeNode 。当冲突链表达到⼀定⻓度时,链表会转换成红⿊树。
15.12、ConcurrentHashMap线程安全的具体实现⽅式/底层具体实现
- JDK1.7(上⾯有示意图):
⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock ,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。 HashEntry ⽤于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组。 Segment 的结构和 HashMap 类似,是⼀种数组和链表结构,⼀个 Segment 包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表结构的元素,每个 Segment 守护着⼀个 HashEntry 数组⾥的元素,当对 HashEntry 数组的数据进⾏修改时,必须⾸先获得对应的 Segment 的锁。
- JDK1.8 (上⾯有示意图):
ConcurrentHashMap 取消了 Segment 分段锁,采⽤ CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红⿊⼆叉树。Java 8 在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红⿊树(寻址时间复杂度为 O(log(N)))
synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并
发,效率⼜提升 N 倍。
15.13、⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
- HashSet :是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
- LinkedHashSet :是 HashSet 的⼦类,能够按照添加的顺序遍历;
- TreeSet :底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排序。
15.14、集合框架底层数据结构总结
1、Collection 接⼝下⾯的集合
1.1.、List
- Arraylist : Object[] 数组
- Vector : Object[] 数组
- LinkedList : 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
1.2、Set
- HashSet (⽆序,唯⼀): 基于 HashMap 实现的,底层采⽤ HashMap 来保存元素
- LinkedHashSet : LinkedHashSet 是 HashSet 的⼦类,并且其内部是通过LinkedHashMap来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现⼀样,不过还是有⼀点点区别的
- TreeSet (有序,唯⼀): 红⿊树(⾃平衡的排序⼆叉树)
2、Map 接⼝下⾯的集合
2.1、Map
- HashMap : JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链
表则是主要为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间 - LinkedHashMap : LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红⿊树组成。另外, LinkedHashMap 在上⾯结构的基础上,增加了⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》
- Hashtable : 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲
突⽽存在的 - TreeMap : 红⿊树(⾃平衡的排序⼆叉树)
15.15、如何选⽤集合?
主要根据集合的特点来选⽤,⽐如我们需要根据键值获取到元素值时就选⽤ Map 接⼝下的集
合,
- 需要排序时选择 TreeMap
- 不需要排序时就选择 HashMap
- 需要保证线程安全就选⽤ConcurrentHashMap 。
当我们只需要存放元素值时,就选择实现 Collection 接⼝下的集合,
- 需要保证元素唯⼀时选择实现Set 接⼝的集合⽐如 TreeSet 或 HashSet
- 不需要就选择实现 List 接⼝的⽐如 ArrayList 或LinkedList ,
- 然后再根据实现这些接⼝的集合的特点来选⽤。
16、多线程
16.1、何为进程?
进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线
程就是这个进程中的⼀个线程,也称主线程。
一个进程可以包含多个线程,各线程之间可以互相通信,共享进程资源。
16.2、何为线程?
线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈
,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。
16.3、请简要描述线程与进程的关系,区别及优缺点?
从JVM角度图解图解进程和线程的关系:
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
总结:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程都是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销,但不利于资源的管理和保护,而进程则相反。
16.4、程序计数器为什么是私有的?
程序计数器主要有下⾯两个作⽤:
- 字节码解释器通过改变程序计数器来依次读取操作指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了。
需要注意的是,如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能够恢复到正确的执行位置。
16.5、虚拟机栈和本地⽅法栈为什么是私有的?
- 虚拟机栈:每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机栈中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机提供执行Java方法的服务(也就是字节码),而本地方法栈则为虚拟机使用到的Native
Native方法服务。在HotSpot虚拟机中和Java虚拟机栈中合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
简单了解堆和⽅法区:
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即是编译器编译后的代码等数据。
16.7、并发与并⾏的区别?
- 并发:同一时间段,多个任务都在执行(单位时间内不一定同时执行)。
- 并行:单位时间内,多个任务同时执行。
16.7、使⽤多线程可能带来什么问题?
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁 。
16.8、什么是上下⽂切换?
多线程编程中⼀般线程的个数都⼤于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
概括来说就是:当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以
便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。
上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。
Linux 相⽐与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有⼀项就是,其上下⽂切换和模式切换的时间消耗⾮常少。
16.9、什么是线程死锁?如何避免死锁?
线程所描述的是这样一种情况:多个线程同时被阻塞,他们中的一个或者全部线程都在等待某个资源的释放。由于线程被无限期的阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。
16.10、如何避免线程死锁?
我上⾯说了产⽣死锁的四个必要条件,为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。现在我们来挨个分析⼀下:
- 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件 :⼀次性申请所有的资源。
- 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。
16.11、sleep() ⽅法和 wait() ⽅法区别和共同点?
- 两者都可以暂停线程的执⾏。
- 两者最主要的区别在于: sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁 。
- sleep() 通常被⽤于暂停执⾏,wait() 通常被⽤于线程间交互/通信。
- sleep() ⽅法执⾏完成后,线程会⾃动苏醒;wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者 notifyAll() ⽅法,或者可以使⽤ wait(long timeout) 超时后线程会⾃动苏醒。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/189413.html