目录
1.4.1 什么是变量、成员变量、局部变量,以及成员变量和局部变量的区别?
2.2 关于final、finally、finalize的区别?
2.4 关于String、StringBuffer和Stringbuilder的区别?
2.6 hashCode()和equals()两种方法是什么关系?
1.关于Java概念
1.1 谈谈对Java的理解?
Java是一门面向对象编程语言,简单易学,吸收了C++语言的优秀特点,摒弃了C++中难以理解多继承、指针等概念,是一种跨平台的编程语言,支持网络编程、多线程,编译与解释并存,具有健壮性和安全性。
- 面向对象的特性:抽象、封装、继承、多态。
- 跨平台特性:“一次编译,处处运行”。Java虚拟机的实现,原理是:Java编译器编译Java代码后生成的是与平台无关的字节码,这些字节码面向JVM,而不同的平台有不同的JVM,因此Java是可以跨平台的。
- JVM和JRE和JDK的关系:JDK是Java开发工具包,JRE是Java虚拟机和Java程序所需的核心类库,JVM就是Java虚拟机。三者的包含关系是JDK > JRE > JDK
- 编译与解释并存:就要提到Java代码执行的三个过程,编译(将源代码(.java)编译成虚拟机可以识别理解的.class字节码文件)、解释(虚拟机执行字节码文件,将字节码翻译成机器能识别的机器码)、运行(对应的机器执行二进制机器码)
1.2 Java的基础数据类型?
数据类型分类和占用内存大小如下图:
1.3 关于面向对象的设计理解
1.3.1 面向对象的特性有哪些?
- 封装:目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。
- 继承:是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果。
- 多态:你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的。
1.3.2 重写和重载的区别?
都是多态的表现形式,只是多态可以分为多态编译时多态(重载)和运行时多态(重写):
- 重载发生在一个类中,方法名相同但方法的入参类型、个数可不相同
- 重写发生在父类和子类中,方法名、入参类型、个数都相同
1.3.3 面向对象的设计原则是什么?
设计模式都是基于此原则展开,也就是SOLID原则,具体如下:
- 单一职责原则(SRP:Single Responsibility Principle):类的功能单一,不能包罗万象,太过杂糅;
- 开放封闭原则(OCP:open-close Principle):对于扩展开放,对于修改封闭;
- 里氏替换原则(LSP):子类可以替换父类能够出现的任何地方(继承是在基类的基础上增加的);
- 接口分离原则(ISP):设计时采用多个与特性客户的接口比通用一个好,方便公用和扩展;
- 依赖倒置原则(DIP):高层次的模块不应该依赖低层次的模块,他们都应该依赖抽象。抽象不依赖具体实现,具体实现依赖抽象。比如出国,你说你是中国人,不能说你是哪个村的人,也就是说不依赖其下某个省县,而是抽象的中国人。
1.4 关于变量与方法的理解?
1.4.1 什么是变量、成员变量、局部变量,以及成员变量和局部变量的区别?
变量是指在程序执行过程中,在某个范围内其值可以变化的量,从本质上讲变量就是内存中的一小块区域。
- 成员变量:方法外部,类的内部定义的变量;作用范围是整个类;随着对象创建而存在,随着对象销毁而消失,因此存储在堆内存中;有默认的初始值。
- 局部变量:类的方法内部中定义的变量;作用范围是方法/语句内;方法被调用、语句被执行时存在,因此存储在栈内存中,方法调用完或者语句结束后自动释放;没有默认的初始值,使用前需要手动赋值。
在使用变量时需要遵循“就近原则”,首先在局部范围找,然后在全局范围找。
1.4.2 静态变量和实例变量的区别?
静态变量:静态变量不属于任何实例对象,是属于类的,所以在内存中只有一份。在类的加载过程中,JVM只为静态变量分配一次存储空间(jdk 1.7放在方法去,jdk 1.8放在堆中)。
实例变量:每次创建对象都会为每个对象分配成员变量的内存空间,实例变量属于实例对象的,在内存中对象创建几次就有几份成员变量(作为对象的一部分,和对象一样保存在堆中)。
1.4.3 静态方法与实例方法的区别?
- 在外部调用静态方法时,可以使用“类名.方法名”的方式,也可以使用“对象名.方法名”的形式,而实例方法只能使用“对象名.方法名”的形式。也就是说,调用静态方法可以不用创建对象。
- 静态方法在访问本类成员时,只能访问静态成员(静态成员变量和静态方法),不允许访问实例成员和实例方法。但实例方法无此限制。
2.关于Java基础知识
2.1 Error和Exception的有什么关系和区别?
Exception和Error的关系:
- Exception 和 Error 都是继承了 Throwable 类:在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch) ,它是异常处理机制的基本组成类型。
- Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类:Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Exception和Error的区别:
- Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
- Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。前面我介绍的不可查的 Error,是 Throwable 不是 Exception。不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
2.2 关于final、finally、finalize的区别?
- final是Java中用来做修饰类、方法、变量的关键字:final修饰类表示该类不可以被继承扩展,final修饰的方法表示该方法不可以被重写,final修饰的变量表示该变量是不可以被修改的;
- finally是用于异常捕获的try...catch...组合使用的,用于保证被捕获代码逻辑后一定要执行的代码逻辑,常用于执行比如关闭数据库连接、手动释放锁等;
- finalize是基础类java.lang.object中的一个方法,它的设计目的是用于保证对象在被垃圾回收期回收前完成资源回收,在JDK9被标记@Deprecated.
2.3 什么是强引用、软引用、弱引用、虚引用?
不同的对象引用类型主要是体现对象不同的可达性状态,影响的是垃圾回收。
- 强引用:就是常见的普通对象引用,只要对象被强引用着垃圾回收器就不会回收它;
MyObject myObject = new MyObject(); //此处 myObject 即是强引用
- 软引用:软引用需要使用SoftReference 来做特殊声明,当系统内存充足时不会回收,在OOM之前会被垃圾回收;
SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
- 弱引用:弱引用需要使用WeakReference类来做特殊声明,不论内存是否充足,只要垃圾回收执行它就会被回收;
SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
- 虚引用:不影响对象的生命周期,在任何时候都可能会被垃圾回收。虚引用其实形同虚设的,作用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制。虚引用必须和引用队列(ReferenceQueue)联合使用,需要使用引用队列(ReferenceQueue)来实现对应的通知机制,被垃圾回收后的虚应用的对象会被放入到引用队列中。
2.4 关于String、StringBuffer和Stringbuilder的区别?
- String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
- StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。
- StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。
- 实现:String是final的,复制修改会产生新对象;StringBuffer和StringBulider方法实现一样,通过append和add拼接,唯一的区别是StringBuffer的方法被Synchonized修饰,简单粗暴影响性能
- 缓存:String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。但是被显式排重的字符串对象放在永久代,使用不当就会产生OOM(在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区)替代了)。
- 自身演化:历史版本中,它是使用 char 数组来存数据的,这样非常直接,但无区别的实现就造成了一定的浪费。在 Java 9 中,我们引入了 Compact Strings 的设计,对字符串进行了大刀阔斧的改进。将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并且将相关字符串操作类都进行了修改。另外,所有相关的 Intrinsic 之类也都进行了重写,以保证没有任何性能损失。
2.5 ==和equals的区别?
- ==的作用是判断两个对象的地址是不是相等,即判断两个对象是不是同一个对象。也就是说:
- 作用于基本数据类型==比较的是值;
- 作用于引用数据类型==比较的是内存地址;
- equals的作用也是判断两个对象是否相等(注意它并不能用于比较基本数据类型)。equals的两种使用情况:
- 类没有重写equals方法:此时通过 equals() 比较该类的两个对象时,等价于通过 == 比较这两个对象,还是相当于比较内存地址。
- 类重写了equals方法:一般来说,我们都会覆盖 equals() 方法来比较两个对象的内容而不是其引用。我们平时覆盖的equals()方法一般是比较两个对象的内容是否相同,自定义了一个相等的标准,也就是两个对象的值是否相等。
2.6 hashCode()和equals()两种方法是什么关系?
- 如果两个对象 equals 相等,则它们必须有相同的哈希码——相同地址的hash值相同。
- 如果两个对象有相同的哈希码,则它们未必 equals 相等——由于hash碰撞,哈希值相等的地址不一定相同。
- 重写equals方法必须重写hashCode是因为:HashSet 首先会调用对象的 hashCode() 方法获取其哈希码,并通过哈希码确定该对象在集合中存放的位置。假设这个位置之前已经存了一个对象,则 HashSet 会进一步调用 equals() 对两个对象进行比较:若相等则说明对象重复,此时不会保存新加的对象,若不等说明对象不重复,但是它们存储的位置发生了碰撞, 此时 HashSet 会采用链式结构在同一位置保存多个对象,即将新加对象链接到原来对象的之后。而 Object 类提供的 equals() 方法默认是用 == 来进行比较的,也就是说只有两个对象是同一个对象时(地址相同),才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。
2.7 深拷贝和浅拷贝的区别?
- 浅拷贝:仅拷贝被拷贝对象的成员变量的值,也就是基本数据类型变量的值,和引用数据类型变量的地址值,而对于引用类型变量指向的堆中的对象不会拷贝。
- 深拷贝:完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份。
2.8 int和Integer的区别?
首先,int是Java的原始基本数据类型,Integer是int的包装类。Integer中有int类型字段存储数据,并且提供了基本操作,比如数学运算、int与字符串之间的转换等。在Java5后,Java引入了自动装箱和自动拆箱机制,可以根据上下文自动进行转换,极大的简化了编程。并且经常数据操作都是一个小范围的数值,因此在Java5中提供了静态工厂方法valueOf()方法,在调用它的时候会有一个缓存机制,缓存范围是-128到127之间。
2.9 接口和抽象类有什么区别?
抽象类是用来捕捉类的通用特性的,接口是抽象方法的集合。从设计层面上讲,抽象类是对类的抽象,是一种模板设计;接口时行为的抽象,是一种行为的规范。
相同点:
- 接口和抽象类都不能被实例化;
- 都位于继承的顶端,用于被其他类实现或继承;
- 都包含抽象方法,其子类必须重写覆盖这些方法。
不同点:
- 声明:抽象类使用abstract关键字;接口使用interface关键字;
- 实现:抽象类使用extends关键字来继承抽象类,如果子类不是抽象类时它需提供抽象类中所有抽象声明的方法实现;接口使用implement关键字来实现,实现类必须提供接口中所有声明的方法的实现;
- 构造器:抽象类可以有构造器;接口不能有构造器;
- 访问修饰符:抽象类中的方法可以是任意访问修饰符;接口中默认是public的,不能被定义为private或者protected;
- 继承:一个类只能继承一个抽象类;一个类可以实现多个接口;
- 字段声明:抽象类的字段声明可以是任意的;接口的字段默认都是static和final的。
Ps:严格说,Java 8 以后,接口也是可以有方法实现的。从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,甚至可以定义 private default method。Default method 提供了一种二进制兼容的扩展已有接口的办法。比如,我们熟知的 java.util.Collection,它是 collection 体系的 root interface,在 Java 8 中添加了一系列 default method,主要是增加 Lambda、Stream 相关的功能。我在专栏前面提到的类似 Collections 之类的工具类,很多方法都适合作为 default method 实现在基础接口里面。
2.10 Java提供哪些IO方式?
传统的IO包java.io中的几种划分方式:
- 按照流划分:输入流和输出流
- 按照操作单元划分:字节流和字符流
- 按照角色划分:字节流和处理流
流的交互方式是:同步、阻塞的。也就是说读写过程中线程会阻塞在那里,他们之间调用时可靠的线性顺序。优点是:交互简单、直观;缺点是:IO的效率和扩展性存在局限,容易形成性能瓶颈。
Java 4中引入了NIO包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。
Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。
2.11 什么是阻塞和非阻塞、同步和异步?
- 在常见IO模型中,线程存在两种状态,分别是:等待就绪和操作状态。阻塞是指线程在没有达到某种条件时会持续等待,不会进行其他操作。非阻塞是指线程不用关心前一个IO是否返回,仍然可以执行一些其他操作。
- 同步是一种可靠的顺序执行机制,严格保证行为的执行顺序,当我们进行同步操作时,后一任务会依赖前面任务的执行完成。相对的,异步就是多个操作中不用保证严格的顺序,在执行A操作的时候B操作也可以同时执行,两者之间没有任何依赖关系。
2.12 描述BIO、NIO、AIO有什么区别?
Java提供的常见IO 模型有3种,分别是: 传统Java.io包中基于流实现的BIO(排队打饭模式)、Java1.4后引入的框架NIO(点单等待被叫模式)、java1.7后引入的AIO(包厢模式)。
BIO:同步阻塞I/O,数据的读取写入必须阻塞在同一线程内,使用简单方便,在单机活动链接数小(小于1000)的情况下可以用,可不用太过于考虑系统过载,线程池本身就是一个天然漏斗,可以缓冲一些处理不了的连接或者请求。但面对并发量更高的场景时不建议使用了。
NIO:同步非阻塞I/O,Java1.4中引入的Java.nio包。提供了Channel、Selector、Buffer等抽象,它提供的是面向缓冲的,基于通道的I/O操作方法。提供了与传统BIO模型中Socket和ServerSocket对应的SocketChannel和ServerSocketChannel两种不同的嵌套字通道实现,两种通道都实现了阻塞和非阻塞两种支持。
Ps:NIO使用多路复用机制实现,即对比NIO的一次只能处理一个连接,NIO支持一个线程连接多个客户端连接,使用Redis的I/O多路复用模型(更多的Redis线程模型在后续缓存篇再整理)。
AIO:异步非阻塞I/O,java1.7中引入。异步IO是基于事件和回调机制实现的,也就是应用操作后会直接返回,不会阻塞在那里,当后台处理完成后操作系统会通知相应的线程进行后续的操作。