面渣逆袭:Java并发六十问,图文详解,快来看看你会多少道!

Posted 三分恶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面渣逆袭:Java并发六十问,图文详解,快来看看你会多少道!相关的知识,希望对你有一定的参考价值。

大家好,我是老三,面渣逆袭 继续,这节我们来盘一盘另一个面试必问知识点——Java并发。

这篇文章有点长,四万字,图文详解六十道Java并发面试题。人已经肝麻了,大家可以点赞收藏慢慢看!扶我起来,我还能肝!

 也叫 黄金分割数hash增量为 这个数字,带来的好处就是 hash 分布非常均匀

    也就是size >= threshold* 3/4来决定是否需要扩容。

作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进⼊同步代码前要获得 当前 class 的锁
指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。

反编译一段synchronized修饰代码块代码,javap -c -s -v -l SynchronizedDemo.class,可以看到相应的字节码指令。

monitorenter和monitorexit
  1. synchronized修饰同步方法时,JVM采用ACC_SYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。

    同样可以写段代码反编译看一下。

synchronized修饰同步方法

synchronized锁住的是什么呢?

monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor实现的。

实例对象结构里有对象头,对象头里面有一块结构叫Mark Word,Mark Word指针指向了monitor

所谓的Monitor其实是一种同步工具,也可以说是一种同步机制。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,可以叫做内部锁,或者Monitor锁。

ObjectMonitor的工作原理:

  • ObjectMonitor有两个队列:_WaitSet、_EntryList,用来保存ObjectWaiter 对象列表。
  • _owner,获取 Monitor 对象的线程进入 _owner 区时, _count + 1。如果线程调用了 wait() 方法,此时会释放 Monitor 对象, _owner 恢复为空, _count - 1。同时该等待线程进入 _WaitSet 中,等待被唤醒。
  • ObjectMonitor() 
        _header       = NULL;
        _count        = 等。

    synchronized做了哪些优化?

    在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,提升了synchronized的性能。

  • 偏向锁:在无竞争的情况下,只是在Mark Word里存储当前线程指针,CAS操作都不做。

  • 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。

  • 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式

  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

  • 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

  • 锁升级的过程是什么样的?

    锁升级方向:无锁-->偏向锁---> 轻量级锁---->重量级锁,这个方向基本上是不可逆的。

    锁升级方向

    我们看一下升级的过程:

    偏向锁:

    偏向锁的获取:

    1. 判断是否为可偏向状态--MarkWord中锁标志是否为‘01’,是否偏向锁是否为‘1’
    2. 如果是可偏向状态,则查看线程ID是否为当前线程,如果是,则进入步骤\'5\',否则进入步骤‘3’
    3. 通过CAS操作竞争锁,如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,然后执行‘5’;竞争失败,则执行‘4’
    4. CAS获取偏向锁失败表示有竞争。当达到safepoint时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块
    5. 执行同步代码

    偏向锁的撤销:

    1. 偏向锁不会主动释放(撤销),只有遇到其他线程竞争时才会执行撤销,由于撤销需要知道当前持有该偏向锁的线程栈状态,因此要等到safepoint时执行,此时持有该偏向锁的线程(T)有‘2’,‘3’两种情况;
    2. 撤销----T线程已经退出同步代码块,或者已经不再存活,则直接撤销偏向锁,变成无锁状态----该状态达到阈值20则执行批量重偏向
    3. 升级----T线程还在同步代码块中,则将T线程的偏向锁升级为轻量级锁,当前线程执行轻量级锁状态下的锁获取步骤----该状态达到阈值40则执行批量撤销

    轻量级锁:

    轻量级锁的获取:

    1. 进行加锁操作时,jvm会判断是否已经时重量级锁,如果不是,则会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象MarkWord复制到该锁记录中
    2. 复制成功之后,jvm使用CAS操作将对象头MarkWord更新为指向锁记录的指针,并将锁记录里的owner指针指向对象头的MarkWord。如果成功,则执行‘3’,否则执行‘4’
    3. 更新成功,则当前线程持有该对象锁,并且对象MarkWord锁标志设置为‘00’,即表示此对象处于轻量级锁状态
    4. 更新失败,jvm先检查对象MarkWord是否指向当前线程栈帧中的锁记录,如果是则执行‘5’,否则执行‘4’
    5. 表示锁重入;然后当前线程栈帧中增加一个锁记录第一部分(Displaced Mark Word)为null,并指向Mark Word的锁对象,起到一个重入计数器的作用。
    6. 表示该锁对象已经被其他线程抢占,则进行自旋等待(默认10次),等待次数达到阈值仍未获取到锁,则升级为重量级锁

    大体上省简的升级过程:

    锁升级简略过程

    完整的升级过程:

    synchronized 锁升级过程-来源参考[14]
    synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(一般是lock()和unlock()方法配合try/finally 语句块来完成。)
  • 性能: 在JDK1.6锁优化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6开始,增加了适应性自旋、锁消除等,两者性能就差不多了。
  • 功能特点: ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。
  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  • synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock类借助Condition接口与newCondition()方法实现。
  • ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁。
  • 下面的表格列出出了两种锁之间的区别:

    synchronized和ReentrantLock的区别
    简单理解,它就是一个管理线程的池子。

    管理线程的池子
  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。
  • 面渣逆袭:Java集合连环三十问

    2021-12-13

    面试字节,被操作系统问挂了

    2021-10-02


    面渣逆袭:Java基础五十三问,两万字图文详解,快来看看有没有你不会的

    大家好,我是老三, 面渣逆袭 系列继续。这节我们回到梦开始的地方——Java基础,其实过了萌新阶段,面试问基础就问的不多,但是保不齐突然问一下。想一下,总不能张口高并发、闭口分布式,结果什么是面向对象,说不清,那多少有点魔幻。所以赶紧来看看,这些基础有没有你不会的!

    Java概述

    1.什么是Java?

    PS:碎怂Java,有啥好介绍的。哦,面试啊。

    Java是一门面向对象的编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的优秀代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。

    2.Java语言有哪些特点?

    Java语言有很多优秀(可吹)的特点,以下几个是比较突出的:

    • 面向对象(封装,继承,多态);
    • 平台无关性,平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。
    • 支持多线程。C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持;
    • 编译与解释并存;

    3.JVM、JDK 和 JRE 有什么区别?

    JVM:Java Virtual Machine,Java虚拟机,Java程序运行在Java虚拟机上。针对不同系统的实现(Windows,Linux,macOS)不同的JVM,因此Java语言可以实现跨平台。

    JRE: Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,Java 命令和其他的⼀些基础构件。但是,它不能⽤于创建新程序。

    JDK: Java Development Kit,它是功能⻬全的 Java SDK。它拥有 JRE 所拥有的⼀切,还有编译器(javac)和⼯具(如 javadoc 和 jdb)。它能够创建和编译程序。

    简单来说,JDK包含JRE,JRE包含JVM。

    4.说说什么是跨平台性?原理是什么

    所谓跨平台性,是指Java语言编写的程序,一次编译后,可以在多个系统平台上运行。

    实现原理:Java程序是通过Java虚拟机在系统平台上运行的,只要该系统可以安装相应的Java虚拟机,该系统就可以运行java程序。

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

    所谓的字节码,就是Java程序经过编译之类产生的.class文件,字节码能够被虚拟机识别,从而实现Java程序的跨平台性。

    Java 程序从源代码到运行主要有三步:

    • 编译:将我们的代码(.java)编译成虚拟机可以识别理解的字节码(.class)
    • 解释:虚拟机执行Java字节码,将字节码翻译成机器能识别的机器码
    • 执行:对应的机器执行二进制机器码

    只需要把Java程序编译成Java虚拟机能识别的Java字节码,不同的平台安装对应的Java虚拟机,这样就可以可以实现Java语言的平台无关性。

    6.为什么说 Java 语言“编译与解释并存”?

    高级编程语言按照程序的执行方式分为编译型解释型两种。

    简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。

    比如,你想读一本外国的小说,你可以找一个翻译人员帮助你翻译,有两种选择方式,你可以先等翻译人员将全本的小说(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。

    Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(\\*.class 文件),这种字节码必须再经过JVM,解释成操作系统能识别的机器码,在由操作系统执行。因此,我们可以认为 Java 语言编译解释并存。

    基础语法

    7.Java有哪些数据类型?

    定义:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。

    Java语言数据类型分为两种:基本数据类型引用数据类型

    基本数据类型:

    • 数值型
      • 整数类型(byte、short、long)
      • 浮点类型(float、long)
    • 字符型(char)
    • 布尔型(boolean)

    Java基本数据类型范围和默认值:

    基本类型位数字节默认值
    int3240
    short1620
    long6480L
    byte810
    char162‘u0000’
    float3240f
    double6480d
    boolean1false

    引用数据类型:

    • 类(class)
    • 接口(interface)
    • 数组([])

    8.自动类型转换、强制类型转换?看看这几行代码?

    Java 所有的数值型变量可以相互转换,当把一个表数范围小的数值或变量直接赋给另一个表数范围大的变量时,可以进行自动类型转换;反之,需要强制转换。

    这就好像,小杯里的水倒进大杯没问题,但大杯的水倒进小杯就不行了,可能会溢出。

    float f=3.4,对吗?

    不正确。3.4 是单精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4;或者写成float f =3.4F

    short s1 = 1; s1 = s1 + 1;对吗?short s1 = 1; s1 += 1;对吗?

    对于 short s1 = 1; s1 = s1 + 1;编译出错,由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。

    而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。

    9.什么是自动拆箱/封箱?

    • 装箱:将基本类型用它们对应的引用类型包装起来;
    • 拆箱:将包装类型转换为基本数据类型;

    Java可以自动对基本数据类型和它们的包装类进行装箱和拆箱。

    举例:

    Integer i = 10;  //装箱
    int n = i;   //拆箱
    

    10.&和&&有什么区别?

    &运算符有两种用法:短路与逻辑与

    &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。

    &&之所以称为短路运算是因为,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&。

    例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。

    注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

    11.switch 是否能作用在 byte/long/String上?

    Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。

    从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。

    从 Java 7 开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

    12.break ,continue ,return 的区别及作用?

    • break 跳出整个循环,不再执行循环(结束当前的循环体)
    • continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
    • return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)

    13.用最有效率的方法计算2乘以8?

    2 << 3。位运算,数字的二进制位左移三位相当于乘以2的三次方。

    14.说说自增自减运算?看下这几个代码运行结果?

    在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(–)。

    ++和–运算符可以放在变量之前,也可以放在变量之后。

    当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。

    例如,当 b = ++a 时,先自增(自己增加 1),再赋值(赋值给 b);当 b = a++ 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。

    用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。

    看一下这段代码运行结果?

    int i  = 1;
    i = i++;
    System.out.println(i);
    

    答案是1。有点离谱对不对。

    对于JVM而言,它对自增运算的处理,是会先定义一个临时变量来接收i的值,然后进行自增运算,最后又将临时变量赋给了值为2的i,所以最后的结果为1。

    相当于这样的代码:

    int i = 1int temp = i;
    i++;
    i = temp;
    System.out.println(i);
    

    这段代码会输出什么?

    int count = 0;
    for(int i = 0;i < 100;i++)
    
        count = count++;
    
    System.out.println("count = "+count);
    

    答案是0。

    和上面的题目一样的道理,同样是用了临时变量,count实际是等于临时变量的值。

    int autoAdd(int count)
    
        int temp = count;
        count = coutn + 1;
        return temp;
    
    

    PS:笔试面试可能会碰到的奇葩题,开发这么写,见一次吊一次。

    面向对象

    15.⾯向对象和⾯向过程的区别?

    • ⾯向过程 :面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的一次调用就可以。
    • ⾯向对象 :面向对象,把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事件在解决整个问题的过程所发生的行为。 目的是为了写出通用的代码,加强代码的重用,屏蔽差异性。

    用一个比喻:面向过程是编年体;面向对象是纪传体。

    16.面向对象有哪些特性

    • 封装

      封装把⼀个对象的属性私有化,同时提供⼀些可以被外界访问的属性的⽅法。

    • 继承

      继承是使⽤已存在的类的定义作为基础创建新的类,新类的定义可以增加新的属性或新的方法,也可以继承父类的属性和方法。通过继承可以很方便地进行代码复用。

    关于继承有以下三个要点:

    1. ⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。

    2. ⼦类可以拥有⾃⼰属性和⽅法,即⼦类可以对⽗类进⾏扩展。

    3. ⼦类可以⽤⾃⼰的⽅式实现⽗类的⽅法。

    • 多态

      所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。

      在 Java 中有两种形式可以实现多态:继承(多个⼦类对同⼀⽅法的重写)和接⼝(实现接⼝并覆盖接⼝中同⼀⽅法)。

    17.重载(overload)和重写(override)的区别?

    方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

    • 重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;

    • 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。

    方法重载的规则:

    1. 方法名一致,参数列表中参数的顺序,类型,个数不同。
    2. 重载与方法的返回值无关,存在于父类和子类,同类中。
    3. 可以抛出不同的异常,可以有不同修饰符。

    18.访问修饰符public、private、protected、以及不写(默认)时的区别?

    Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

    • default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
    • private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
    • public : 对所有类可见。可以修饰类、接口、变量、方法
    • protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)

    19.this关键字有什么作用?

    this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针

    this的用法在Java中大体可以分为3种:

    1. 普通的直接引用,this相当于是指向当前对象本身

    2. 形参与成员变量名字重名,用this来区分:

    public Person(String name,int age)
        this.name=name;
        this.age=age;
    
    
    1. 引用本类的构造函数

    20.抽象类(abstract class)和接口(interface)有什么区别?

    1. 接⼝的⽅法默认是 public ,所有⽅法在接⼝中不能有实现(Java 8 开始接⼝⽅法可以有默认实现),⽽抽象类可以有⾮抽象的⽅法。

    2. 接⼝中除了 static 、 final 变量,不能有其他变量,⽽抽象类中则不⼀定。

    3. ⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。接⼝⾃⼰本身可以通过 extends 关键字扩展多个接⼝。

    4. 接⼝⽅法默认修饰符是 public ,抽象⽅法可以有 public 、 protected 和 default 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)。

    5. 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。

    1. 在 JDK8 中,接⼝也可以定义静态⽅法,可以直接⽤接⼝名调⽤。实现类和实现是不可以调⽤的。如果同时实现两个接⼝,接⼝中定义了⼀样的默认⽅法,则必须重写,不然会报错。

    2. jdk9 的接⼝被允许定义私有⽅法 。

    总结⼀下 jdk7~jdk9 Java 中接⼝的变化:

    1. 在 jdk 7 或更早版本中,接⼝⾥⾯只能有常量变量和抽象⽅法。这些接⼝⽅法必须由选择实现接⼝的类实现。

    2. jdk 8 的时候接⼝可以有默认⽅法和静态⽅法功能。

    3. jdk 9 在接⼝中引⼊了私有⽅法和私有静态⽅法。

    21.成员变量与局部变量的区别有哪些?

    1. 从语法形式上看:成员变量是属于类的,⽽局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

    2. 从变量在内存中的存储⽅式来看:如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。

    3. 从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调⽤⽽⾃动消失。

    4. 成员变量如果没有被赋初值:则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值。

    22.静态变量和实例变量的区别?静态方法、实例方法呢?

    静态变量和实例变量的区别?

    静态变量: 是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个副本。

    实例变量: 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。

    静态⽅法和实例⽅法有何不同?

    类似地。

    静态方法:static修饰的方法,也被称为类方法。在外部调⽤静态⽅法时,可以使⽤"类名.⽅法名"的⽅式,也可以使⽤"对象名.⽅法名"的⽅式。静态方法里不能访问类的非静态成员变量和方法。

    实例⽅法:依存于类的实例,需要使用"对象名.⽅法名"的⽅式调用;可以访问类的所有成员变量和方法。

    24.final关键字有什么作用?

    final表示不可变的意思,可用于修饰类、属性和方法:

    • 被final修饰的类不可以被继承

    • 被final修饰的方法不可以被重写

    • 被final修饰的变量不可变,被final修饰的变量必须被显式第指定初始值,还得注意的是,这里的不可变指的是变量的引用不可变,不是引用指向的内容的不可变。

      例如:

              final StringBuilder sb = new StringBuilder("abc");
              sb.append("d");
              System.out.println(sb);  //abcd
      

      一张图说明:

    25.final、finally、finalize的区别?

    • final 用于修饰变量、方法和类:final修饰的类不可被继承;修饰的方法不可被重写;修饰的变量不可变。

    • finally 作为异常处理的一部分,它只能在 try/catch 语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit (0) 可以阻断 finally 执行。

    • finalize 是在 java.lang.Object 里定义的方法,也就是说每一个对象都有这么个方法,这个方法在 gc 启动,该对象被回收的时候被调用。

      一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。

    26.==和 equals 的区别?

    == : 它的作⽤是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象(基本数据类型**比较的是值,引⽤数据类型**比较的是内存地址)。

    equals() : 它的作⽤也是判断两个对象是否相等。但是这个“相等”一般也分两种情况:

    • 默认情况:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象,还是相当于比较内存地址。

    • 自定义情况:类覆盖了 equals() ⽅法。我们平时覆盖的 equals()方法一般是比较两个对象的内容是否相同,自定义了一个相等的标准,也就是两个对象的值是否相等。

    举个例⼦,Person,我们认为两个人的编号和姓名相同,就是一个人:

    public class Person 
        private String no;
        private String name;
    
        @Override
        public boolean equals(Object o) 
            if (this == o) return true;
            if (!(o instanceof Person)) return false;
            Person person = (Person) o;
            return Objects.equals(no, person.no) &&
                    Objects.equals(name, person.name);
        
    
        @Override
        public int hashCode() 
            return Objects.hash(no, name);
        
    
    

    27.hashCode与 equals?

    这个也是面试常问——“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写hashCode ⽅法?”

    什么是HashCode?

    hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数,定义在 Object 类中, 是一个本地⽅法,这个⽅法通常⽤来将对象的内存地址转换为整数之后返回。

    public native int hashCode();
    

    哈希码主要在哈希表这类集合映射的时候用到,哈希表存储的是键值对(key-value),它的特点是:能根据“键”快速的映射到对应的“值”。这其中就利⽤到了哈希码!

    为什么要有 hashCode?

    上面已经讲了,主要是在哈希表这种结构中用的到。

    例如HashMap怎么把key映射到对应的value上呢?用的就是哈希取余法,也就是拿哈希码和存储元素的数组的长度取余,获取key对应的value所在的下标位置。详细可见:面渣逆袭:Java集合连环三十问

    为什么重写 quals 时必须重写 hashCode ⽅法?

    如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。反之,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。

    hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)

    为什么两个对象有相同的 hashcode值,它们也不⼀定是相等的?

    因为可能会碰撞, hashCode() 所使⽤的散列算法也许刚好会让多个对象传回相同的散列值。越糟糕的散列算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode )。

    28.Java是值传递,还是引用传递?

    Java语言是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。

    JVM 的内存分为堆和栈,其中栈中存储了基本数据类型和引用数据类型实例的地址,也就是对象地址。

    而对象所占的空间是在堆中开辟的,所以传递的时候可以理解为把变量存储的对象地址给传递过去,因此引用类型也是值传递。

    29.深拷贝和浅拷贝?

    • 浅拷贝:仅拷贝被拷贝对象的成员变量的值,也就是基本数据类型变量的值,和引用数据类型变量的地址值,而对于引用类型变量指向的堆中的对象不会拷贝。
    • 深拷贝:完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份。

    例如现在有一个order对象,里面有一个products列表,它的浅拷贝和深拷贝的示意图:

    因此深拷贝是安全的,浅拷贝的话如果有引用类型,那么拷贝后对象,引用类型变量修改,会影响原对象。

    浅拷贝如何实现呢?

    Object类提供的clone()方法可以非常简单地实现对象的浅拷贝。

    深拷贝如何实现呢?

    • 重写克隆方法:重写克隆方法,引用类型变量单独克隆,这里可能会涉及多层递归。
    • 序列化:可以先讲原对象序列化,再反序列化成拷贝对象。

    30.Java 创建对象有哪几种方式?

    Java中有以下四种创建对象的方式:

    • new创建新对象
    • 通过反射机制
    • 采用clone机制
    • 通过序列化机制

    前两者都需要显式地调用构造方法。对于clone机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制需要明确其实现原理,在Java中序列化可以通过实现Externalizable或者Serializable来实现。

    常用类

    String

    31.String 是 Java 基本数据类型吗?可以被继承吗?

    String是Java基本数据类型吗?

    不是。Java 中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type)。

    String是一个比较特殊的引用数据类型。

    String 类可以继承吗?

    不行。String 类使用 final 修饰,是所谓的不可变类,无法被继承。

    32.String和StringBuilder、StringBuffer的区别?

    • String:String 的值被创建后不能修改,任何对 String 的修改都会引发新的 String 对象的生成。
    • StringBuffer:跟 String 类似,但是值可以被修改,使用 synchronized 来保证线程安全。
    • StringBuilder:StringBuffer 的非线程安全版本,性能上更高一些。

    33.String str1 = new String(“abc”)和String str2 = “abc” 和 区别?

    两个语句都会去字符串常量池中检查是否已经存在 “abc”,如果有则直接使用,如果没有则会在常量池中创建 “abc” 对象。

    但是不同的是,String str1 = new String(“abc”) 还会通过 new String() 在堆里创建一个 “abc” 字符串对象实例。所以后者可以理解为被前者包含。

    String s = new String(“abc”)创建了几个对象?

    很明显,一个或两个。如果字符串常量池已经有“abc”,则是一个;否则,两个。

    当字符创常量池没有 “abc”,此时会创建如下两个对象:

    • 一个是字符串字面量 “abc” 所对应的、字符串常量池中的实例
    • 另一个是通过 new String() 创建并初始化的,内容与"abc"相同的实例,在堆中。

    34.String不是不可变类吗?字符串拼接是如何实现的?

    String的确是不可变的,“+”的拼接操作,其实是会生成新的对象。

    例如:

    String a = "hello ";
    String b = "world!";
    String ab = a + b;
    

    jdk1.8之前,a和b初始化时位于字符串常量池,ab拼接后的对象位于堆中。经过拼接新生成了String对象。如果拼接多次,那么会生成多个中间对象。

    内存如下:

    Java8时JDK对“+”号拼接进行了优化,上面所写的拼接方式会被优化为基于StringBuilder的append方法进行处理。Java会在编译期对“+”号进行处理。

    下面是通过javap -verbose命令反编译字节码的结果,很显然可以看到StringBuilder的创建和append方法的调用。

    stack=2, locals=4, args_size=1
         0: ldc           #2                  // String hello
         2: astore_1
         3: ldc           #3                  // String world!
         5: astore_2
         6: new           #4                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        13: aload_1
        14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: aload_2
        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: astore_3
        25: return
    
    

    也就是说其实上面的代码其实相当于:

    String a = "hello ";
    String b = "world!";
    StringBuilder sb = new StringBuilder();
    sb.append(a);
    sb.append(b);
    String ab = sb.toString();
    

    此时,如果再笼统的回答:通过加号拼接字符串会创建多个String对象,因此性能比StringBuilder差,就是错误的了。因为本质上加号拼接的效果最终经过编译器处理之后和StringBuilder是一致的。

    当然,循环里拼接还是建议用StringBuilder,为什么,因为循环一次就会创建一个新的StringBuilder对象,大家可以自行实验。

    35.intern方法有什么作用?

    JDK源码里已经对这个方法进行了说明:

         * <p>
         * When the intern method is invoked, if the pool already contains a
         * string equal to this @code String object as determined by
         * the @link #equals(Object) method, then the string from the pool is
         * returned. Otherwise, this @code String object is added to the
         * pool and a reference to this @code String object is returned.
         * <p>
    

    意思也很好懂:

    • 如果当前字符串内容存在于字符串常量池(即equals()方法为true,也就是内容一样),直接返回字符串常量池中的字符串
    • 否则,将此String对象添加到池中,并返回String对象的引用

    Integer

    36.Integer a= 127,Integer b = 127;Integer c= 128,Integer d = 128;,相等吗?

    答案是a和b相等,c和d不相等。

    • 对于基本数据类型==比较的值
    • 对于引用数据类型==比较的是地址

    Integer a= 127这种赋值,是用到了Integer自动装箱的机制。自动装箱的时候会去缓存池里取Integer对象,没有取到才会创建新的对象。

    如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用缓存池中的Integer对象,超过范围 a1==b1的结果是false

        public static void main(String[] args) 
            Integer a = 127;
            Integer b = 127;
            Integer b1 = new Integer(127);
            System.out.println(a == b); //true
            System.out.println(b==b1);  //false
    
            Integer c = 128;
            Integer d = 128;
            System.out.println(c == d);  //false
        
    

    什么是Integer缓存?

    因为根据实践发现大部分的数据操作都集中在值比较小的范围,因此 Integer 搞了个缓存池,默认范围是 -128 到 127,可以根据通过设置JVM-XX:AutoBoxCacheMax=来修改缓存的最大值,最小值改不了。

    实现的原理是int 在自动装箱的时候会调用Integer.valueOf,进而用到了 IntegerCache。

    很简单,就是判断下值是否在缓存范围之内,如果是的话去 IntegerCache 中取,不是的话就创建一个新的Integer对象。

    IntegerCache是一个静态内部类, 在静态块中会初始化好缓存值。

     private static class IntegerCache 
         ……
         static 
                //创建Integer对象存储
                for(int k = 0; k < cache.length; k++)
                    cache[k] = new Integer(j++);
             ……
          
     
    

    37.String怎么转成Integer的?原理?

    PS:这道题印象中在一些面经中出场过几次。

    String转成Integer,主要有两个方法:

    • Integer.parseInt(String s)
    • Integer.valueOf(String s)

    不管哪一种,最终还是会调用Integer类内中的parseInt(String s, int radix)方法。

    抛去一些边界之类的看看核心代码:

    public static int parseInt(String s, int radix)
                    throws NumberFormatException
        
    
            int result = 0;
            //是否是负数
            boolean negative = false;
            //char字符数组下标和长度
            int i = 0, len = s.length();
            ……
            int digit;
            //判断字符长度是否大于0,否则抛出异常
            if (len > 0) 
                …… 
                while (i < len) 
                    // Accumulating negatively avoids surprises near MAX_VALUE
                    //返回指定基数中字符表示的数值。(此处是十进制数值)
                    digit = Character.digit(s.charAt(i++),radix);
                    //进制位乘以数值
                    result *= radix;        
                    result -= digit;
                
             
            //根据上面得到的是否负数,返回相应的值
            return negative ? result : -result;
        
    
    

    去掉枝枝蔓蔓(当然这些枝枝蔓蔓可以去看看,源码cover了很多情况),其实剩下的就是一个简单的字符串遍历计算,不过计算方式有点反常规,是用负的值累减。

    Object

    38.Object 类的常见方法?

    Object 类是一个特殊的类,是所有类的父类,也就是说所有类都可以调用它的方法。它主要提供了以下 11 个方法,大概可以分为六类:

    对象比较