java面试题第一章基础知识

1.1数据类型

1,为什么要设计封装类,Integer和int区别

Integer 是基本数据类型 int 的封装类
在 Java 里面,有八种基本数据类型,他们都有一一对应的封装类型。
基本类型和封装类型的区别有很多,比如
1, int 类型,我们可以直接定义一个变量名赋值即可,但是 Integer 需要使用 new 关键字创建对象,
2, 基本类型和 Integer 类型混合使用时,Java 会自动通过拆箱和装箱实现类型转换
3,Integer 作为一个对象类型,封装了一些方法和属性,我们可以利用这些方法来操作数据。
4,作为成员变量,Integer 的默认值是 null,而 int 的默认值是 0
5,在 Java 里面,之所以要对基础类型设计一个对应的封装类型。是因为 Java 本身是一门面向对象的语言,对象是 Java 语言的基础单元,我们时时刻刻都在创建对象,也随时都在使用对象,很多时候在传递数据时也需要对象类型,比如像 ArrayList、HashMap 这些集合,只能存储对象类型。
其次,封装类型还有很多好处,比如
 安全性较好,可以避免外部操作随意修改成员变量的值,保证了成员变量和数据传递的安全性
 隐藏了实现细节,对使用者更加友好,只需要调用对象提供的方法就可以完成对应的操作

2,为什么1000 = = 1000为false, 100 = = 100为true

问题分析

首先, Integer a1=100, 把一个 int 数字赋值给一个封装类型,Java 会默认进行装箱操作,也就是调用 Integer.valueOf()方法,把数字 100 包装成封装类型 Integer。
其次,在 Integer 内部设计中,用到了享元模式的设计,享元模式的核心思想是通过复用对象,减少对象的创建数量,从而减少内存占用和提升性能。
Integer 内部维护了一个 IntegerCache,它缓存了-128 到 127 这个区间的数值对应的 Integer 类型。一旦程序调用 valueOf 方法,如果数字是在-128 到 127 之间就直接在cache 缓存数组中去取Integer 对象。否则,就会创建一个新的对象。
所以,对于这个面试题来说,两个Integer对象,因为值都是100,并且默认通过装箱机制调用了valueOf方法。
从 IntegerCache 中拿到了两个完全相同的 Integer 实例。因此用等号比较得到的结果必然是 true。

回答

a1==a2 的执行结果是 true
原因是 Integer 内部用到了享元模式的设计,针对-128 到 127 之间的数字做了缓存。
使用 Integer a1=100 这个方式赋值时,Java 默认会通过 valueOf 对 100 这个数字进行装箱操作,
从而触发了缓存机制,使得 a1 和 a2 指向了同一个 Integer 地址空间。

3,new String(“abc”)到底创建了几个对象?

  1. 如果 abc 这个字符串常量不存在,则创建两个对象,分别是 abc 这个字符串常量,以及 new String这个实例对象。
  2. 如果 abc 这字符串常量存在,则只会创建一个对象。

4, String、StringBuffer、StringBuilder 区别

关于 String、StringBuffer、StringBuilder 的区别,我想从四个角度来说明。
第一个,可变性,
String 内部的 value 值是 final 修饰的,所以它是不可变类。所以每次修改 String 的值,都会产生一个新的对象。
StringBuffer 和 StringBuilder 是可变类,字符串的变更不会产生新的对象。
第二个,线程安全性,
String 是不可变类,所以它是线程安全的。
StringBuffer 是线程安全的,因为它每个操作方法都加了 synchronized 同步关键字。
StringBuilder 不是线程安全的。
所以在多线程环境下对字符串进行操作,应该使用 StringBuffer,否则使用 StringBuilder
第三个,性能方面。
String 的性能是最低的,因为不可变意味着在做字符串拼接和修改的时候,需要重新创建新的对象以及分配内存。
其次是 StringBuffer 要比 String 性能高,因为它的可变性使得字符串可以直接被修改
最后是 StringBuilder,它比 StringBuffer 的性能高,因为 StringBuffer 加了同步锁。
第四个,存储方面。
String 存储在字符串常量池里面,StringBuffer 和 StringBuilder 存储在堆内存空间。 StringBuilder 和 StringBuffer 都是派生自 AbstractStringBuilder 这个抽象类。

1.2 对象

1,java对象的创建过程

在实例化一个对象的时候,JVM 首先会去检查目标对象是否已经被加载并初始化了。
如果没有,JVM 需要立刻去加载目标类,然后调用目标类的构造器完成初始化。 目标类的加载是通过类加载器来实现的,主要就是把一个类加载到内存里面。
然后初始化的过程,主要是对目标类里面的静态变量、成员变量、静态代码块进行初始化。

当目标类被初始化以后,就可以从常量池里面找到对应的类元信息,并且目标对象的大小在类加载之后就已经确定了,所以这个时候就需要为新创建的对象,根据目标对象的大小在堆内存里面分配内存空间。
内存分配的方式一般有两种,一种指针碰撞,另一种是空闲列表,JVM 会根据 Java 堆内存是否规整来决定内存分配方式。

接下来,JVM 会把目标对象里面的普通成员变量初始化为零值,比如 int 类型初始化为 0,对象类型初始化为 null,(类变量在类加载的准备阶段就已经初始化过了)。
这一步操作主要是保证对象里面的实例字段,不用初始化就可以直接使用,也就是程序能够获得这些字段对应数据类型的零值。
然后,JVM 还需要对目标对象的对象头做一些设置,比如对象所属的类元信息、对象的 GC 分代年龄、hashcode、锁标记等等。

完成这些步骤以后,接下来要做的,就是执行目标对象内部生成的 init 方法,初始化成员变量的值、执行构造块、最后执行目标对象的构造方法,完成对象的创建。
其中,init 方法是 Java 文件编译之后在字节码文件中生成的,它是一个实例构造器,这个构造器会把语句块、变量初始化、调用父类构造器等操作组织在一起。所以调用 init 方法能够完成一系列的初始化动作。
在这里插入图片描述

2,深拷贝和浅拷贝

深拷贝和浅拷贝是用来描述对象或者对象数组这种引用数据类型的复制场景的。
浅拷贝,(如图)就是只复制某个对象的指针,而不复制对象本身。
这种复制方式意味着两个引用指针指向被复制对象的同一块内存地址。
在这里插入图片描述

深拷贝,(如图)会完全创建一个一模一样的新对象,新对象和老对象不共享内存,也就意味着对新对象的修改不会影响老对象的价值。
在这里插入图片描述

无论是深拷贝还是浅拷贝,都需要通过实现 Cloneable 接口,并实现 clone()方法。
然后我们可以在 clone()方法里面实现浅拷贝或者深拷贝的逻辑。
实现深拷贝的方法有很多,比如

  1. 通过序列化的方式实现,也就是把一个对象先序列化一遍,然后再反序列化回来,就会得到一个完整的新对象。
  2. 在 clone()方法里面重写克隆逻辑,也就是对克隆对象内部的引用变量再进行一次克隆。

3,强引用,弱应用,软引用,虚引用有什么区别

不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。
强引用,就是普通对象的引用,只要还有强引用指向一个对象,就能表示对象还“活着”,
垃圾收集器无法回收这一类对象。
只有在没有其他引用关系,或者超过了引用的作用域,再或者显示的把引用赋值为 null 的时候,垃圾回收器才能进行内存回收。
软引用,是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。
软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用,相对强引用而言,它允许在存在引用关联的情况下被垃圾回收的对象在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,垃圾回收器都会回收该内存
虚引用,它不会决定对象的生命周期,它提供了一种确保对象被 finalize 以后,去做某些事情的机制。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入与之关联的引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收,然后我们就可以在引用的对象的内存回收之前采取必要的行动。

4,一个空Object到底占用多大内存

在开启了压缩指针的情况下,Object 默认会占用 12 个字节,
但是为了避免伪共享问题,JVM 会按照 8 个字节的倍数进行填充,所以会填充 4 个字节变成 16 个字节长度。
在关闭压缩指针的情况下,Object 默认会占用 16 个字节,16 个字节正好是 8 的整数倍,因此不需要填充。
在 HotSpot 虚拟机里面,(如图)一个对象在堆内存里面的内存布局是使用 OOP 结构来表示的,
它主要分为三个部分。
在这里插入图片描述

(如图)对象头,包括 Markword、类元指针、数组长度
其中 Markword 用来存储对象运行时的相关数据,比如 hashCode、gc 分代年龄等。
在 64 位操作系统中占 8 个字节,32 位操作系统中占 4 个字节
类元指针指向当前实例对象所属哪个类,开启指针压缩的情况下占 4 个字节,未开启则占 8 个字节
数组长度只有对象数组才会存在,占 4 个字节
 实例数据,存储对象中的字段信息
 对齐填充,Java 对象的大小需要按照 8 个字节或者 8 个字节的倍数对齐,避免伪共享问题。
在这里插入图片描述

因此,一个空的对象,在开启压缩指针的情况下,占 16 个字节
其中 Markword 占 8 个字节、类元指针占 4 个字节, 对齐填充占 4 个字节。

5,为什么重写equels方法就一定要重写hashCode()方法

问题分析

关于这个问题,首先需要深入了解一下 equals 这个方法。
这个 equals 方法是 String 这个类里面的实现。
从代码中可以看到,当调用 equals 比较两个对象的时候,会做两个操作

  1. 用==号比较两个对象的内存地址,如果地址相同则返回 true
  2. 否则,继续比较字符串的值,如果两个字符串的值完全相等,同样返回 true
    在这里插入图片描述

那 equals 和 hashCode()有什么关系呢?
 首先,Java 里面任何一个对象都有一个native 的 hashCode()方法
 其次,这个方法在散列集合中会用到,比如 HashTable、HashMap 这些,当添加元素的时候,需要判断元素是否存在,
而如果用 equals 效率太低,所以一般是直接用对象的 hashCode 的值进行取模运算。
 如果 table 中没有该 hashcode 值,它就可以直接存进去,不用再进行任何比较了;
 如果存在该 hashcode 值, 就调用它的 equals 方法与新元素进行比较,相同的话就不存了,
不相同就散列其他的地址,所以这里存在一个冲突解决的问题,这样一来实际调用 equals 方法的次数就大大降低了. hashCode 的值默认是 JVM 使用随机数来生成的,两个不同的对象,可能生成的 HashCode 会相同。
这种情况在 Hash 表里面就是所谓的哈希冲突,通常会使用链表或者线性探测等方式来解决冲突问题。
但是如果两个完全相同的对象,也就是内存地址指向同一个,那么他们的 hashCode 一定是相同的。
了解了 equals 和 hashCode 的关系以后,再来分析这个面试题。
在理论情况下,如果 x.equals(y)==true,如果没有重写 equals 方法,那么这两个对象的内存地址是同一个,意味着 hashCode 必然相等。
但是如果我们只重写了 equals 方法,就有可能导致 hashCode 不相同。
一旦出现这种情况,就导致这个类无法和所有集合类一起工作。
所以,在实际开发中,约定俗成了一条规则,重写 equals 方法的同时也需要重写hashCode 方法。

回答

如果只重写 equals 方法,不重写 hashCode 方法。
就有可能导致 a.equals(b)这个表达式成立,但是 hashCode 却不同。
那么这个只重写了 equals 方法的对象,在使用散列集合进行存储的时候就会出现问题。
因为散列结合是使用 hashCode 来计算 key 的存储位置,如果存储两个完全相同的对象,但是有不同的 hashcode就会导致这两个对象存储在 hash 表的不同位置,当我们想根据这个对象去获取数据的时候,就会出现一个悖论
一个完全相同的对象会存储在 hash 表的两个位置,造成大家约定俗成的规则,出现一些不可预料的错误。

1.3 其他特性

2,受检异常和非受检异常

  1. 一、首先是异常的本质
    受检异常和非受检异常,都是继承自 Throwable 这个类中,分别是 Error 和Exception,Error 是程序报错,系统收到无法处理的错误消息,它和程序本身无关。
    Excetpion 是指程序运行时抛出需要处理的异常信息如果不主动捕获,则会被 jvm 处理。
  2. 二、然后是对受检异常和非受检异常的定义
    前面说过受检异常和非受检异常均派生自Exception 这个类。
  3. 受检异常的定义是程序在编译阶段必须主动捕获的异常,遇到该异常有两种处理方法通过 try/catch 捕获该异常或者通过 throw 把异常抛出去
  4. 非受检异常的定义是程序不需要主动捕获该异常,一般发生在程序运行期间,比如NullPointException
  5. 三、最后我还可以说下他们优点和缺点
    受检异常优点有两个:
     第一,它可以响应一个明确的错误机制,这些错误在写代码的时候可以随时捕获并且能很好地提高代码的健壮性。
     第二,在一些连接操作中,它能很好地提醒我们关注异常信息,并做好预防工作。
    不过受检异常的缺点是:抛出受检异常的时候需要上声明,而这个做法会直接破坏方法签名导致版本不兼容。这个恶心特性导致我会经常使用 RuntimeException 包装。
    非受检异常的好处是可以去掉一些不需要的异常处理代码,而不好之处是开发人员可能忽略某些应该处理的异常,导致带来一些隐藏很深的 Bug,比如忘记关闭?连接忘记释放等。

3,fail-fast机制和fail-saft机制分别有什么用

fail-safe 和 fail-fast ,是多线程并发操作集合时的一种失败处理机制。
Fail-fast : 表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出 异常,从而导致遍历失败。
定义一个 Map 集合,使用 Iterator 迭代器进行数据遍历,在遍历过程中,对集合数据做变更时,就会发生 fail-fast。
java.util 包下的集合类都是快速失败机制的, 常见的使用 fail-fast 方式遍历的容器有HashMap 和ArrayList 等
Fail-safe,表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出Exception。
原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所做的修改并不能被迭代器检测到。
java.util.concurrent 包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。
常见的使用 fail-safe 方式遍历的容器有ConcerrentHashMap 和CopyOnWriteArrayList 等。

4,如何理解序列化和反序列化

首先,我认为,之所以需要序列化,核心目的是解决网络通信之间的对象传输问题。
也就是说,如何把当前 JVM 进程里面的一个对象,跨网络传输到另外一个 JVM 进程里面。
而序列化,就是把内存里面的对象转化为字节流,以便用来实现存储或者传输。
反序列化,就是根据从文件或者网络上获取到的对象的字节流,根据字节流里面保存的对象描述信息和状态。重新构建一个新的对象。
其次呢,序列化的前提是保证通信双方对于对象的可识别性,所以很多时候,我们会把对象先转化为通用的解析格式,比如 json、xml 等。
然后再把他们转化为数据流进行网络传输,从而实现跨平台和跨语言的可识别性。
最后,我再补充一下序列化选择。
那在实际应用里面,哪种序列化最合适,我认为有几个关键因素。
 序列化之后的数据大小,因为数据大小会影响传输性能
 序列化的性能,序列化耗时较长会影响业务的性能
 是否支持跨平台和跨语言
 技术的成熟度,越成熟的方案使用的公司越多,也就越稳定。

5,什么事SPI,有什么用

它是 JDK 内置的一种动态扩展点的实现。
(如图),简单来说,就是我们可以定义一个标准的接口,然后第三方的库里面可以实现这个接口。
那么,程序在运行的时候,会根据配置信息动态加载第三方实现的类,从而完成功能的动态扩展机制。
在这里插入图片描述

在 Java 里面,SPI 机制有一个非常典型的实现案例,就是数据库驱动 java.jdbc.Driver
(如图),JDK 里面定义了数据库驱动类 Driver,它是一个接口,JDK 并没有提供实现。
具体的实现是由第三方数据库厂商来完成的。
在程序运行的时候,会根据我们声明的驱动类型,来动态加载对应的扩展实现,从而完成数据库的连接。
在这里插入图片描述

除此之外,在很多开源框架里面都借鉴了 Java SPI 的思想,提供了自己的 SPI 框架,比如
Dubbo 定义了 ExtensionLoader,实现功能的扩展。
Spring 提供了 SpringFactoriesLoader,实现外部功能的集成。
以上就是我对这个问题的理解!

6,finally语句快一定会执行吗

问题解析
这个问题,很明显是考察 Java 基础。
finally 语句块在实际开发中使用得非常多,它是和 try 语句块组合使用,通常情况下,不管有没有触发异常,finally 语句块中的代码是必然会执行的
所以我们会把资源的释放,或者业务日志的打印放在 finally 语句块里面。

回答

finally 语句块在两种情况下不会执行:
 程序没有进入到 try 语句块因为异常导致程序终止,这个问题主要是开发人员在编写代码的时候,异常捕获的范围不够。
 在 try 或者 cache 语句块中,执行了 System.exit(0)语句,导致 JVM 直接退出

7,什么是内存溢出,什么是内存泄漏。

1、什么是内存溢出?

发生内存溢出。
我们来看到右侧的区域,假设我们 JVM 中可用的内存空间只剩下 3M,但是我们要创建一个 5M 的对象,那么,新创建的对象就放不进去了。这个时候,我们就叫做内存溢出。

2、什么是内存泄漏?

它代表业务代码执行时,所需要占用的内存空间。这段业务代码中创建了两个 1M 的对象,一起会占用 2M 内存。当对象使用完之后,这两个对象并没有释放,因此内存中会留下 2M 的内存空间一直被占用。而我们的业务代码在程序中会被反复执行,每次执行都会留下 2M 不被释放,反复执行多次之后,随着时间的累积,就会有大量的对象用完不被释放,导致这些对象不能得到回收而发生内存溢出,这种情况就叫做内存泄漏。
也就说,在我们的业务代码执行过程中,有些对象它应该被回收,但是又有其他对象引用它,因此,GC 不能自动回收。所以,该回收的垃圾对象没有被回收,垃圾对象越堆越多,可用内存越来越少,若可用内存无法存放新的垃圾对象,最终导致内存泄漏。内存泄漏最终会导致内存溢出。

3、如何避免?

我们在 Code 过程中,特别是一些流对象,比如 OutputStream,Reader,BitMap,Document,很容易忘了 Close。最麻烦的是还要顺序回收,顺序错了还产生空指针,所以,大家在 Code 过程一定要注意,当然,现在有很多 IDE 会智能提示,也避免了很多低级错误。

  • 9
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值