Java基础汇总

1. JDK、JRE、JVM三者的区别和联系

JDK:Java Development Kit Java开发工具,提供给java开发人员使用的。
JRE:Java Runtime Environment Java运行时环境,提供给运行java程序的用户使用的。
JVM:Java Virtual Machine Java虚拟机,解释.class文件,使得操作系统可以执行。
它们三者的关系图:
简单来说,JDK包含JRE,JRE包含JVM。
在这里插入图片描述

2. == 和 equals

== :比的是栈中的值,基本数据类型比的是变量值,引用数据类型比的是地址值。
equals:在object中底层默认也是跟==一样;但是,我们常用的类,比如String通常会重写equals,使其比较的是值是否一致。
object中equals:
在这里插入图片描述
String中重写后的equals:
在这里插入图片描述

3. final相关的问题

final(最终的)的作用

  • 修饰类:表示类不可被继承,言外之意就是不能修饰抽象类(abstract);
  • 修饰方法:表示方法不可被子类覆盖(重写),但是可以重载,重载和重写的区别下有说明;
  • 修饰变量:表示一但被赋值就不可以更改它的值。
    (1). 修饰成员变量
    如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明时赋值。
    如果final修饰的是成员变量,可以在非静态初始化块中、声明该变量时或者构造器中赋值。
    (2). 修饰局部变量
    系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以在后面代码中对其赋值;但是,在使用该变量之前必须赋值,而且只能赋一次值。
    (3). 修饰基本数据类型数据和引用数据类型
    如果修饰的是基本数据类型的变量,则其数值一但初始化就不能更改;
    如果修饰的是引用数据类型的变量,则其初始化之后便不能再让其指向另一个对象,但是引用的值是可以改变的。即地址值不能改变,值是可以改变的。如图所示:
    在这里插入图片描述
    未使用final修饰,则可以修改:
    在这里插入图片描述

4. String、StringBuffer、StringBuilder三者的区别及其使用场景

String底层是final修饰的,不可变的,每次操作都会产生新的String对象;
StringBuffer和StringBuilder都是在原对象上操作,StringBuffer是线程安全的,StringBuilder是线程不安全的;StringBuffer方法底层使用了synchronized修饰。
性能:StringBuilder > StringBuffer > String
使用场景:如果经常需要修改字符串类容时,使用StringBuilder ,在多线程环境下定义共享变量时使用StringBuffer;如果对定义了就不经常进行修改的值,可以直接使用String,比如订单号、手机号等。

5. 重载和重写的区别

重载:发生在同一个类中,方法名必须相同;参数类型、参数个数、参数顺序不同;方法的返回值和访问修饰符可以不同。发生在编译时,即编译就会报错。如图:
在这里插入图片描述
将其参数类型改正一下,则构成重载:
在这里插入图片描述
重写:发生在子父类中,方法名、参数类型、参数个数、参数顺序必须相同;返回值范围小于等于父类;抛出的异常范围小于等于父类;访问权限大于等于父类;如果父类方法权限为私有的(private),则其子类就不能重写该方法。

6. 接口和抽象类的区别

  • 抽象类可以存在普通成员方法,而接口只能存在public abstract 方法。
  • 抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final了类型的,即常量。
  • 抽象类只能继承一个,接口可以实现多个。
  • 说明:接口的设计目的是对类的行为进行约束;也就是提供一种机制,可以强制要求不同类具有相同的行为,它只约束了行为的有无,不对如何实现进行限制。
    抽象类的设计,是代码复用;当不同的类具有相同的行为,且其中一部分行为的实现方式一致时,可以让这些类派生于一个抽象类;从而达到代码复用的目的,所以抽象类只能被继承,不能实例化出来。
  • 使用场景:当我们关注一个事务的本质的时候,用抽象类;当我们关注一个操作的时候,用接口。

7. List和Set的区别

List:有序,按对象进入的顺序保存对象,可重复,允许多个null元素对象,可以使用iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素。
Set:无序,不可重复,最多只允许一个null元素对象,取元素时只能使用iterator取出所有元素,再逐一遍历。

8. hashCode和equals

hashCode() 的作用是获取哈希码,也称散列码,它实际上返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置,hashCode()方法定义在JDK的Object.java中,java中的任何类都包含hashCode()方法。哈希表存储的是键值对(key-value),它的特点是能根据key快速检索出对应的value。这其中就是利用到了哈希码,可以快速找到所需要的对象。

为什么要有hashCode?
以hashSet查重为例:
对象加入hashSet时,hashSet会先计算对象的hashCode值来判断对象加入的位置,看该位置是否有值;如果没有,hashSet会假设对象无重复,直接加入。但是如果发现有值,这时会调用equals() 方法来检查两个对象是否相同,如果两者相同,哈希表就不会让其加入;如果不同,就会重新散列到其他位置。这样就大大减少了equals的次数,相对就大大提高了执行速度。

hashCode和equals区别与联系:

  • 如果两个对象相等,则hashCode一定是相同的。
  • 两个对象相等,他们的equals方法返回true。
  • 两个对象的hashCode相同,他们不一定相等。
  • 重写equals方法时也必须重写hashCode方法。
  • hashCode()方法默认是对堆上的对象产生独特值,如果没有重写hashCode(),则该class的两个对象无论何时都不会相等。

9. ArrayList和LinkedList的区别

ArrayList:基于动态数组,连续的内存存储,适合随机访问;扩容机制:因为数组长度固定,超出长度数据时需要新建数组,然后将老数组的数据拷贝到新数组,默认长度为10(JDK8中初始化时,默认创建的是一个空数组,在第一次使用时将其容量指定为10,实现了懒加载的作用),默认扩容是其原数组的1.5倍。JDK8下的ArrayList源码如下图:

数组初始容量:
在这里插入图片描述
在这里插入图片描述
添加操作,如果不指定初始化长度,或者指定长度小于10,则默认长度就为10:
在这里插入图片描述
在这里插入图片描述
扩容:
在这里插入图片描述
当然,这里额外补充一点,ArrayList在多线程环境下是不安全的,在多线程我们通常使用CopyOnWriteArrayList这个类。添加操作源码如下,使用了lock锁,这样保证了在多线程环境下的线程安全问题:
在这里插入图片描述

LinkedList:基于双端链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询。它没有固定容量,不需要扩容;内存开销比较大,很少使用,这里就不详细介绍了。

10. HashMap和HashTable的区别以及底层实现?

区别:

  • HashMap方法没有synchronized修饰,线程不安全,HashTable线程安全
  • HashMap允许key和value为null,HashTable不允许
    底层实现:数组+链表+红黑树(JDK8)
    -元素以内部类Node节点存在。
    • 存储方式:计算key的hash值,二次hash后与数组的长度取模,对应到数组的下标;如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组。如果产生hash冲突,先进项equals比较,如果相同则取代该元素;如果不同,则判断链表高度插入链表,如果链表高度达到8,并且数组长度达到64,则将链表转换成红黑树;当长度低于6时将红黑树转回链表。
    • key为null时,存储在下标为0的位置。
    • 数组扩容:HashMap底层构造hash表时,如果不指定初始大小,则默认数组的大小为16,如果数组中的元素达到0.75*数组长度(0.75是默认的扩容因子,如果默认长度为16,则达到12便会触发扩容机制),则会触发扩容,变为原来2倍。
      在这里插入图片描述
      特别说明,HaspMap扩容是非常繁琐耗时的,我们在使用时,如果确定需要的长度大于16的话,尽量指定其长度,避免它触发扩容。

11. JDK8中ConcurrentHashMap原理

  • 数据结构:synchronized+CAS+Node+红黑树,Node的val和next都是用volatile修饰,保证其可见性;查找、替换、赋值操作都使用CAS。
  • 锁:锁链表的head节点,不影响其他元素的读写,锁的粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容。
  • 读操作无锁:Node的val和next使用volatile修饰,读写线程对该变量互相可见;数组用volatile修饰,保证扩容时被读线程感知,所以不会读到脏数据。
    在这里插入图片描述
    添加操作:
    在这里插入图片描述
    在这里插入图片描述

12. 什么是字节码,采用字节码的好处?

在Java中,字节码就是供JVM虚拟机理解的代码,它不面向任何特定的处理器,只面向JVM虚拟机。这也是为什么Java程序为什么能一次编译,到处运行的原因。
好处:Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保证了解释型语言可移植的特点;由于字节码并不针对某一种特定的机器,所以这使得Java程序可以在装了JVM虚拟机的任何操作系统上运行。

13. Java类加载器以及它们的路径?

JDK自带有三个类加载器:bootstrap ClassLoader(引导类/启动类加载器)、ExtClassLoader(扩展类加载器)、AppClassLoader(系统类/应用程序类加载器);此外,我们还可以自定义类加载器。

  • bootstrap ClassLoader(引导类/启动类加载器):它是ExtClassLoader的父类加载器,默认加载%JAVA_HOME%lib下的jar包和class文件。
  • ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%lib/ext文件夹下的jar包和class文件。
  • AppClassLoader是所有自定义类加载的父类,它也是线程上下文加载器,负责加载classpath下的文件。
  • 如果我们需要自定义类加载器,则继承classLoader即可。
    它们的关系如下:
    在这里插入图片描述

14. 双亲委派机制

简单来讲,当我们拿到一个类,它首先不会自己去加载,而是向上委派查找缓存,如果上面有缓存就直接返回;如果没有就继续向上委派查找缓存,直到查找到启动类;如果启动类还是没有缓存,则从启动类开始向下查找加载路径,查找到自定义类加载器为止,找到了就直接返回;如果仍然未找到,则报错;常见的错误如下:
在这里插入图片描述

双亲委派的好处:

  • 为了安全性,避免用户自己编写的类动态替换Java的一些核心类,如上图所示。
  • 同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,如果相同的class文件被不同的classLoader加载就是两个不同的类。

15. Java中的异常体系

  • Java中所有的异常都来自于顶级父类Throwable(异常体系)。
  • Throwable下面有两个子类:Exception(异常)和Error(错误)。
  • Error是程序无法处理的错误,一但出现这个错误,则程序将被迫停止运行。
  • Exception不会导致程序停止,又分为两个部分:RuntimeException(运行时异常)和CheckedException(检查异常)。
  • RuntimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过,通常IDE会提示,所以一般不会发生在运行的程序中。以上是比较粗略的概括,细化如下图所示:在这里插入图片描述

16. GC如何判断对象可以被回收?

常见的判断对象可以被回收方法有两种:引用计数法和可达性分析法。

  • 引用计数法:每个对象有一个引用计数器,默认为1,新增一个引用计时器时,计数器+1;释放一个引用计时器时,计数器-1。当引用计时器为0时,则说明可以被回收。(Java并不采用此方法,而是采用可达性分析法)
  • 可达性分析法:每次触发GC时,都从GC Roots 开始向下搜索,搜索中所走过的路程称为引用链;当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的,JVM则判断是可回收对象。

GC Roots的对象有:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 堆中静态变量引用的对象
  • 方法区(JDK8中元空间)中常量引用的对象
  • 本地方法栈中JNI引用的对象
    说明:可达性分析法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被宣告死亡前至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连的引用链;第二次是在由JVM自动建立的Finalizer(最终)队列中判断是否需要执行finalizer()方法。
    当对象变成GC Roots 不可达时,GC会判断该对象是否覆盖了finalizer()方法,若未覆盖,则直接回收。否则,若对象为执行过finalizer()方法,则将其放入F-Queue队列,由低优先级线程执行该对象中的finalizer()方法;每个对象只能触发一次finalizer()方法。执行finalizer()方法完毕后,GC会再次判断该对象是否可达,若不可达则回收,否则对象“复活”。

17. 线程的生命周期,线程的状态有哪些?

Java中的线程有以下6中状态:

  • 创建(NEW):新创建了一个线程对象,但还没有调用start()方法。
  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  • 阻塞(BLOCKED):表示线程阻塞于锁。
  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  • 终止(TERMINATED):表示该线程已经执行完毕。源码如下:

在这里插入图片描述

18. sleep()、wait()、join()、yield()的区别

首先,我们来了解两个基本概念:锁池和等待池。
锁池:所有需要竞争同步锁的线程都会放在锁池当中;比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池中进行等待;当前面的线程释放同步锁后,锁池中的线程去竞争同步锁,当某个线程得到同步锁后会进入就绪队列,等待CPU分配资源。
等待池:当我们调用wait()方法后,线程会方到等待池中,等待池的线程是不会去竞争同步锁的。只有调用了notify() 方法或者 notifyAll() 方法后等待池的线程才会开始去竞争锁,notify() 是随机从等待池选出一个线程放到锁池,而notifyAll() 是将等待池的所有线程放到锁池中。

  • sleep() 是Thread类中的静态本地方法,wait() 是Object类的本地方法。
  • sleep() 方法不会释放lock,但是wait() 方法会释放,而且会加入到等待队列中。
  • sleep() 方法不依赖synchronized同步监视器,wait() 依赖同步监视器。
  • sleep() 不需要被唤醒(休眠到指定时长后自动退出阻塞),但wait() 需要被唤醒(不指定时间)。
  • sleep() 一般用于当前线程休眠,或者轮循暂停操作,wait() 则用于多线程之间的通信。
  • sleep() 会让出CPU执行时间且强制切换上下文,而wait() 则不一定,wait() 后可能还是有机会重新竞争到锁继续执行。
  • yield() 执行后,线程直接进入就绪状态;马上释放CPU的执行权,但是依然保留了CPU的执行资格,所以有可能CPU下次进项线程调度还会让这个线程获取到执行权继续执行。
  • join() 执行后线程进入阻塞状态。比如线程B中调用线程A的join(),那么线程B就会进入到阻塞队列,直到A结束或中断。

19. ThreadLocal使用场景

  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  • 线程间数据隔离。
  • 进行事务操作,用于存储线程事务信息。
  • 数据库连接,session会话管理。

20. ThreadLocal如何避免内存泄露问题

我们首先来了解以下几个概念:

  • 内存泄露:即程序员在申请内存后,无法释放已申请的内存空间,简而言之就是不再使用的对象占据内存空间不能被回收。一但堆积后,最终可能会导致OOM。
  • 内存泄露的原因:ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应的key就会导致内存泄露问题。
  • ThreadLocal避免内存泄露的方法:
    1.每次使用完ThreadLocal都调用它的remove()方法清除数据
    2.将ThreadLocal变量定义为private static,这样就一直存在ThreadLocal的强引用,也就保证了任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而进行回收。

21. 并发、并行、串行

  • 并发允许多个任务彼此干扰,统一时间点、只有一个任务运行,多个任务交替执行。
  • 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
  • 串行就是每个任务依次执行。

22. 并发的三大特性

原子性、可见性、有序性

23. 为什么用线程池,解释下线程池的参数?

  • 降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
  • 提高响应速度;任务来了直接有线程可用可执行,而不是先创建线程,在执行。
  • 提高线程的可管理性,线程是稀缺资源,使用线程池可以统一分配调优监控。

线程池的7大参数:
(1)corePoolSize:线程池中的常驻核心线程数。
(2)maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1。
(3)keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。
(4)unit:keepAliveTime的单位。
(5)workQueue:任务队列,被提交但尚未被执行的任务。
(6)threadFactory:表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可。
(7)handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maxnumPoolSize)时如何来拒绝请求执行的runnable的策略。源码如下图:
在这里插入图片描述

24. 线程池处理流程

如图所示:
在这里插入图片描述

25. 线程池中任务队列(阻塞队列)的作用,为什么是先添加到任务队列而不是先创建最大线程执行任务?

阻塞队列作用:

  • 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了。阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
  • 阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait(等待)状态,释放CPU资源。
  • 阻塞队列自带阻塞和唤醒功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用CPU资源

为什么是先添加到任务队列:
在线程池中,创建新线程的时候,是需要获取全局锁的,这个时候其它的线程就得阻塞,影响了整体效率。

26. 线程复用的原理?

  • 线程池将线程和任务进行解耦,线程是线程,任务是任务;摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制。
  • 在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()
    来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则执行,也就是调用任务中的run方法,将run方法当成一个普通方法执行,通过这种方式只使用固定的线程,就能将所有任务的run方法串联起来。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jming956

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值