常见问题
Java 面向对象编程三大特性(封装、继承、多态)
- 封装:把一个对象的属性私有化,同时提供能访问这些属性的方法
- 继承:继承是使用已存在的类的定义作为基础建立新类,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但是不能选择性的继承父类
- 多态:指的是程序中定义的引用变量所指向的具体类型以及该引用对象发出的方法调用并不确定,而是在程序运行期间才确定
(Java 中有两种形式可以实现多态:继承、接口实现)
String 为什么是不可变的?String、StringBuffer 和 StringBuilder 的区别是什么?
- 可变性
String 类中使用 final 关键字修饰字符数组来保存字符串 private final char value[]
,所以String对象是不可变的
而 StringBuffer 和 StringBuilder 也是用字符数组来保存,但是没有用final修饰,所以是可变的
- 线程安全性
String 中的对象是不可变的,可以理解为常量,所以是线程安全的
StringBuffer 对方法加了同步锁,所以是线程安全的
StringBuilder 没有对方法加锁
- 性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象
StringBuffer 每次都会对本身进行操作
StringBuilder 因为没有加锁,所以会获得一定的性能提升,但是需要冒线程不安全的风险
总结
操作少量数据:String
单线程操作字符缓冲区下大量数据:StringBuilder
多线程操作字符缓冲区下大量数据:StringBuffer
自动装箱与拆箱
对于 Integer 来说,通过 valueOf 创建 Integer 对象时,如果值在[-128, 127]之间,便返回指向 IntegerCache.cache 中已经存在的对象的引用;否则创建一个新的 Integer 对象,这个范围可以通过 VM 参数更改
(对于 Integer、Short、Byte、Character、Long 这几个类来说实现是类似的)
对于 Boolean 来说,内部会定义两个静态类,TRUE、FALSE
在一个静态方法内调用一个非静态成员为什么是非法的
因为静态方法可以不通过对象进行调用,因此在静态方法内,不能调用其他非静态变量,也不能访问非静态变量成员
== 与 equals(重要)
== : 它的作用是判断两个对象的地址是否是相等的,即判断两个对象是不是同一个对象(基本类型比较的是值,引用类型比较的内存地址)
equals:它的作用也是判断对象是否相等,但是分为两种情况
没有覆盖 equals 方法:默认使用 == 比较
覆盖 equals 方法:可以自己实现来比较两个对象是否相同,如果内容相同则返回 true
说明
String 的 equals 被重写过,比较的是对象的值
创建 String 对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把地址赋给当前引用,如果没有则在常量池中重新创建一个对象
hashCode 和 equals(重要)
- hashCode()介绍
继承自 Object 的 hashCode 方法是本地方法,该方法通常用来将对象的内存地址转换成整数后返回
- 为什么要有 hashCode
hashCode 通常用在 Hash 数据结构确定位置
例如 HashSet,对象在加入 HashSet 时,会先计算对象的 hashCode 来判断加入位置,同时也会和其他 hashCode 做比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现过
但是如果发现有相同的 hashCode 值的对象,这时会调用 equals 方法判断是否真的相同。如果相同加入就不会成功
- 为什么重写 equals 时必须要重写 hashCode 方法?
因为两个对象如果相同,那他们的 hashCode 也一定是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true
hashCode 的默认行为是对堆上的对象产生独特值,如果没有重写 hashCode 方法,那么 hashCode 无论如何也不会相等
- 为什么两个对象具有相同的hashCode值,他们也不一定是相同的?
hashCode 所使用的杂凑算法也许刚好让多个对象返回相同的杂凑值,越糟糕的算法越容易碰撞
hashCode 只是用来缩小查找成本,equals 用来判断是否真的相同
BIO、NIO、AIO 有什么区别?
- BIO(Blocking I/O):同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成
在活动连接数不是特别高(小于1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或者请求。
线程发起 IO 请求后,一直阻塞 IO,直到缓冲区数据准备就绪后,在进入下一步操作
- NIO(Non-Blocking / New I/O):同步非阻塞 I/O 模型。线程发起 IO 请求后,立即返回
同步指的是必须等待 IO 缓冲区的数据就绪,而非阻塞指的是,用户线程不原地等待 IO 缓冲区,可以先做一些其他操作,但是要定时轮询检查 IO 缓冲区是否就绪
Java 中的 NIO 是 New IO 的意思,其实是 NIO + IO 多路复用技术。普通的 NIO 查询是线程轮询查看一个 IO 缓冲区是否就绪,而 Java 中的 NewIO 指的是线程轮询查看一批 IO 缓冲区是否就绪,这就是 IO 多路复用的思想。IO 多路复用模型中,将检查 IO 数据是否就绪的任务交给系统级别的 select 或 epoll 模型,有系统进行监控,减轻用户线程负担
- AIO(Asynchronous):真正意义上的异步非阻塞模型。上述 IO 实现中,需要用户线程定时轮询,去检查 IO 缓冲区数据是否就绪,占用应用程序线程资源,其实轮询相当于还是阻塞的,并非真正解放当前线程,因为它还是需要去查询哪些 IO 就绪。真正理想的异步非阻塞 IO 应该让系统完成,用户只需要告诉系统,当缓冲区就绪后,通知我或者执行我交给你的回调函数
Java 中的 IO 原理
Java 中的 IO 都是依赖操作系统内核进行的,我们程序中的 IO 读写其实调用的是操作系统内核中的 read&write 两大系统调用
内核是怎么进行 IO 交互的?
- 网卡收到网络数据,并将网络数据写到内存中
- 当网卡把数据写到内存后,网卡向 CPU 发出一个中断请求,操作系统便能得知有新数据来,再通过网卡中断程序去处理数据
- 将内存中的数据写入到对应 socket 的缓冲区中
- 当缓冲区的数据写好之后,应用程序开始进行数据处理
同步和异步
同步和异步指的是一个执行流程中每个方法是否必须依赖前一个方法完成后才可以继续执行
同步和异步关注的是方法的执行方是主线程还是其他线程,主线程的话需要等待方法执行完成,其他线程无需等待立刻返回方法调用,主线程可以直接执行接下来的代码
阻塞与非阻塞
阻塞与非阻塞指的是单个线程内遇到同步等待时,是否在原地不做任何操作
举个例子
烧一壶水需要半个小时
A 去烧水,就这样干等了半个小时,然后开始喝热水(BIO)
B 去烧水,但是需要等半个小时,于是去做其他事情,时不时的过来看水有没有烧好。在这段时间内,他既做了其他事情,也烧好了水(NIO)
C 买了一个高级水壶,水烧开之后会发出叫声,于是 C 不用干等着,也不用时不时的过来看,因为水开了他会听到,最后也喝上了热水(AIO)