👨🎓博主主页:爪哇贡尘拾Miraitow
📆创作时间:🌴2022年2月7日🌴
📒内容介绍:开始记录面试题
📚参考资料:帅地
路人张java面试
公众号文章
⏳简言以励:列位看官,且将新火试新茶,诗酒趁年华
📝内容较多有问题希望能够不吝赐教🙏
🎃 欢迎点赞 👍 收藏 ⭐留言 📝
【面试题】java基础篇
1、JVM、JRE及JDK的关系
JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。
JRE( Java Runtime Environment)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。
简单来说就是JDK是Java的开发工具,JRE是Java程序运行所需的环境,JVM是Java虚拟机.它们之间的关系是JDK包含JRE和JVM,JRE包含JVM
2、JAVA语言特点
- Java是一种
面向对象的语言
- Java通过Java虚拟机实现了平台无关性,
一次编译,到处运行
- 支持
多线程
- 支持
网络编程
- 具有较高的
安全性
和可靠性
3、java面向对象编程的三大特征
3.1封装
封装
:
把一个对象的属性私有化(private),同时提供一些可以被外界访问的属性的方法(get set),如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
3.2 继承
继承
:
是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类/基类),得到继承信息的被称为子类(派生类)。
3.3多态
多态
:
分为编译时多态(方法重载)
和运行时多态(方法重写)
。要实现多态需要做两件事:一是子类继承父类并重写父类中的方法,二是用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。
总结:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
4、为什么 Java 中只有值传递?
按值调用
:(call by value)表示方法接收的是调用者提供的值,
按引用调用
:(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。
下面通过 3 个例子来给大家说明
4.1 example 1 基本数据类型
结果:
a = 2
b = 1
num1 = 1
num2 = 2
解析:
在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。
通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.
4.2 example 2 对象类型
结果:
1
0
解析:
首先要知道,数组类型也是一个对象,array 是 arr 的拷贝也就是说array也是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此外部对引用对象的改变会反映到所对应的对象上。
通过上图我们已经看到,实现一个改变对象类型参数状态的方法并不是一件难事。方法得到的是对象引用的拷贝,对象引用及其它的拷贝同时引用同一个对象。
很多人认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。
4.3 example 3
结果:
x:小李
y:小张
s1:小张
s2:小李
解析:
如果是按值传递
,那么s1和s2所引用的对象应该被交换了,然而,方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝,在方法结束时参数变量X和y被丢弃了。原来的变量s1和s2仍然引用这个方法调用之前所引用的对象
总结
Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。
下面再总结一下 Java 中方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以修改一个对象类型对的参数。
- 一个方法不能让对象参数引用一个新的对象。
参考:
《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节
重写
重写是子类对父类的允许访问的方法的实现过程进行重新编写
方法的重写(override)两同两小一大原则:
- 方法名相同,参数类型相同
- 子类返回类型小于等于父类方法返回类型
- 子类抛出异常小于等于父类方法抛出异常
- 子类访问权限大于等于父类方法访问权限
另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法
5、 equals和==
==
: 记住此句话即可(基本数据类型==比较的是值,引用数据类型比较的是内存地址)。
equals()
: 它的作用也是判断两个对象是否相等。
- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
举个例子:
说明:
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- 采用 String aa = “ab” 方式 ,当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
6、hashcode和equals
面试官可能会问你:
你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?
我们以类的用途来将“hashCode() 和 equals()的关系”分2种情况来说明。
第一种 不会创建类对应的散列表
这里所说的“不会创建类对应的散列表”是说:我们不会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类。例如,不会创建该类的HashSet集合。在这种情况下,该类的“hashCode() 和 equals() ”没有关系的!这种情况下,equals() 用来比较该类的两个对象是否相等。而hashCode() 则根本没有任何作用,所以,不用理会hashCode()。
第二种 会创建类对应的散列表
以下所有讨论都是建立在会创建散列表的前提下讨论的:
6.1 hashCode()介绍
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。 虽然,每个Java类都包含hashCode() 函数。
我们都知道,散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!散列表的本质是通过数组实现的。当我们要获取散列表中的某个“值”时,实际上是要获取数组中的某个位置的元素。而数组的位置,就是通过“键”来获取的;更进一步说,数组的位置,是通过“键”对应的散列码计算得到的,下面将进一步详细说明hashCode的作用。
6.2 为什么要有 hashCode
我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()
方法来检查 hashcode 相等的对象是否真的相同。如果两者相同
,HashSet 就不会让其加入操作成功。如果不同
的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度
。如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
通过我们可以看出:hashCode()
的作用就是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置
。hashCode()在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
7、 为什么重写equals时必须重写hashCode方法
由上面的叙述可以知道如果不重写hashcode 方法,即使两个对象内容相等,那它们的hashCode()不等;所以,HashSet在添加两个相等元素的时候,认为它们不相等,导致HashSet中有重复元素,这是不行的。实例代码如下
运行结果:
p1.equals(p2) : true; p1(1169863946) p2(1690552137)
set:[(eee, 100), (eee, 100), (aaa, 200)]
可以发现:我们重写了Person的equals()。但是,很奇怪的发现:HashSet中仍然有重复元素:p1 和 p2。由此验证了我们刚才的推理。(简单来说就是我们先比较的Hashcode是不相等,所以没有比较equals,直接添加了,我们通过equals我们可以知道,这两个对象是相同的,显然不符合我们的set特点无序且唯一
)
7.1 hashCode()与equals()的相关规定
- 如果
两个对象相等
,则hashcode
一定也是相同的 - 两个
对象相等
,对两个对象分别调用equals方法都返回true - 两个对象有相同的hashcode值,它们也不一定是相等的(
哈希冲突
) - hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
8、异常
8.1 Java异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 Throwable类。Throwable: 有两个重要的子类:Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 执行操作所需的内存资源不足时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 异常由Java虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、ArithmeticException(算术运算异常,一个整数除以0时,抛出该异常)和 ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。
除了以上的分类,异常还能分为非检查异常
和检查异常
- 非检查异常(unckecked exception):该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有 try-catch 捕获它,也没有使用 throws 抛出该异常,编译也会正常通过。因为这样的异常发生的原因很可能是代码写的有问题。
- 检查异常(checked exception):除了 Error 和 RuntimeException 的其它异常。这是编译器要求必须处理的异常。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,所以必须处理这些异常。
所有Exception的直接子类都叫做编译时异常
编译时异常是编译阶段发生的嘛?
不是,编译时异常表示必须在编写程序的时候预先对这种异常进行处理,如果不处理编译器报错
运行时异常一定要处理嘛?
不是,运行时异常在编写程序阶段,你可以处理也可以不去处理
8.2 Throwable类常用方法
Methods | 简述 |
---|---|
public string getMessage() | 返回异常发生时的简要描述 |
public string toString() | 返回异常发生时的详细信息 |
public string getLocalizedMessage() | :返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同 |
public void printStackTrace() | 在控制台上打印Throwable对象封装的异常信息 |
8.3 异常处理总结
- try 块: 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
- catch 块: 用于处理try捕获到的异常。
- finally 块: 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return 语句时,finally语句块将在方法返回之前被执行。
在以下4种特殊情况下,finally块不会被执行:
- 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行
- 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数(exit是java关闭虚拟机的意思) ;若该语句在异常语句之后,finally会执行
- 程序所在的线程死亡。
- 关闭CPU。
9、 final finally finallize区别
final:用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、被其修饰的类不可继承;
finally:异常处理语句结构的一部分,表示总是执行;
finallize:Object类的一个方法,在垃圾回收时会调用被回收对象的finalize
10、字符型常量和字符串常量的区别?
-
形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符
-
含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
-
占内存大小 字符常量只占2个字节; 字符串常量占若干个字节 (注意: char在Java中占两个字节)
10.1、 String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]
,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
但是没有用 final 关键字修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的
AbstractStringBuilder.java
线程安全性
String 中的对象是不可变
的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁
或者对调用的方法加了同步锁,
所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
StringBuffer 补充
说明:StringBuffer 中并不是所有方法都使用了 Synchronized 修饰来实现同步:
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用String
- 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer
11、 深拷贝 vs 浅拷贝
-
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。(浅拷贝不是说只是像下面这样,而是也创建了一个新的对象,如下面的默认的clone方法一样)
Employee original = new Employee("John Public", 50000); Employee copy = original;
-
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
12、String类的常用方法有哪些?
Methods | 简述 |
---|---|
ndexOf() | 返回指定字符的索引。 |
charAt() | 返回指定索引处的字符。 |
replace() | 字符串替换。 |
trim() | 去除字符串两端空白。 |
split() | 分割字符串,返回一个分割后的字符串数组。 |
getBytes() | 返回字符串的 byte 类型数组。 |
length() | 返回字符串长度。 |
toLowerCase() | 将字符串转成小写字母。 |
toUpperCase() | 将字符串转成大写字符。 |
substring() | 截取字符串。 |
equals() | 字符串比较 |
13、 Object类常用方法
Methods | 简述 |
---|---|
Object clone() | 创建与该对象的类相同的新对象 |
boolean equals(Object) | 比较两对象是否相等 |
void finalize() | 当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法 |
Class getClass() | 返回一个对象运行时的实例类 |
int hashCode() | 返回该对象的散列码值 |
void notify() | 唤醒等待在该对象的监视器上的一个线程 |
void notifyAll() | 唤醒等待在该对象的监视器上的全部线程 |
String toString() | 返回该对象的字符串表示 |
void wait() | 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线 程等待 |
13、什么是java的序列化,如何实现序列化
对象序列化是一个用于将对象状态
转换为字节流
的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序。从字节流创建对象的相反的过程称为反序列化。而创建的字节流是与平台无关的,在一个平台上序列化的对象可以在不同的平台上反序列化。序列化是为了解决在对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化的类实现 Serializable 接口,该接口没有需要实现的方法,只是用于标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream 对象,接着使用 ObjectOutputStream 对象的 writeObject(Object obj) 方法可以将参数为 obj 的对象写出,要恢复的话则使用输入流。
14、java中反射是什么意思?有哪些应用场景?
举个栗子:
public static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
Class.forName(JDBC_DRIVER);
//以上也可以直接替换为 new com.mysql.jdbc.Driver();
Class.forName(String className)返回的是一个类,在这个过程中,会把该类加载到jvm中
,即这个类的静态代码会执行,我们主要就是为了要个静态代码块(java.sql.DriverManager.registerDriver(new Driver());)执行才加载这个驱动的。
为什么不使用new com.mysql.jdbc.Driver()这种方式呢?
如果使用new com.mysql.jdbc.Driver()这种方式,会对这个具体的类产生依赖。后续如果你要更换数据库驱动,就得重新修改代码。而使用反射的方式,只需要在配置文件中,更改相应的驱动和url即可。----解耦
每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName(“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
(1)Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
(2)Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
(3)Constructor :可以用 Constructor 创建新的对象。
应用举例:工厂模式,使用反射机制,根据全限定类名获得某个类的 Class 实例。
15、泛型擦除
Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。看下面
输出结果
true
可以看到 ArrayList 和 ArrayList 的原始类型是相同,在编译成字节码文件后都会变成 List ,JVM看到的只有 List ,看不到泛型信息,这就是泛型的类型擦除。
在看下面这段代码
输出
1
a
可以看到通过反射进行 add 操作, ArrayList 竟然可以存储字符串,这是因为在反射就是在运行期调用的 add 方法,在运行期泛型信息已经被擦除。
既然存在类型擦除,那么Java是如何保证在 ArrayList 添加字符串会报错呢?
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
16、JAVA序列化
序列化:将对象写入到IO流中
反序列化:从IO流中恢复对象
序列化的意义:将Java对象转换成字节序列,这些字节序列更加便于通过网络传输或存储在磁盘上,在需要时可以通过反序列化恢复成原来的对象。
实现方式:
- 实现Serializable接口
- 实现Externalizable接口
序列化的注意事项:
- 对象的类名、实例变量会被序列化;方法、类变量、 transient 实例变量都不会被序列化。
- 某个变量不被序列化,可以使用 transient 修饰。
- 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
- 反序列化时必须有序列化对象的 class 文件。
17、Java IO流
主要可以分为输入流和输出流。按照照操作单元划分,可以划分为字节流和字符流。按照流的角色划分为节点流和处理流。
Java I0流的40多个类都是从4个抽象类基类中派生出来的。
InputStream:字节输入流
Reader:字符输入流
OutputStream:字节输出流
Writer:字符输出流