Java 面试宝典

一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅知识面试题,也是你 Java 知识点的扫盲贴。

  • 回答两个目的:节约内存和提升性能。在Java9之前,String对象内部是通过一个char数组来存储字符的,每个char占用2个字节(使用的UTF-16编码)。这就导致一些String中的字符都是拉丁字母(例如英文、数字)也是占用2个字节,但实际上它们只需要一个字节就足够了,导致了内存的浪费。在Java9中,String对象内部表示由一个char数组改变为使用byte数组,同时使用一个编码标记(coder)用于指示String是使用哪种编码格式存储的:Latin-1或者UTF-16:如果String只包含拉丁字母,那么它会使用ISO-8859-1编码,每个字符只占用一个字节如果String还包含其他字符,那么它会使用UTF-16编码,每个字符占用2个或更多字节。这种改变使得Java9能够在不牺牲兼容性的前提下,根据字符串的实际内容动态选择最节省空间的编码方式,从而减少内存消耗。同时,由于S

    2024-03-14
    阅读(150)
  • 回答在Java中,String、StringBuffer和StringBuilder是处理字符串的三个核心类,他们的差异主要体现在不可变性、线程安全性和性能三个方面。不可变性String是不可变的,即当我们新建一个String对象后,其底层数据是不会发生改变的,任何能够改变String的方法都是新建一个String对象,原对象不会发生改变。StringBuffer和StringBuilder都是可变的,意味着我们可以在原对象上对其进行修改操作。线程安全性由于String是不可变的,所以它天然具备线程安全。StringBuffer也是线程安全的,它的所有公共方法都使用了同步机制(synchronized),意味着在多线程环境下,StringBuffer是不会允许两个线程同时操作的。StringBuilder不是线程安全的,它和StringBuffer的实现机制其实是非常相似的,只不过Stri

    2024-03-14
    阅读(128)
  • 回答a==b是为true的。因为"a"+"b"是一个字符串常量表达式,Java编译器在编译时期就会将其优化,直接替换为结果"ab"。因此,Stringb="ab"。所以Stringa="ab";和Stringb="a"+"b";实际上都会引用字符串常量池中的同一个"ab"字符串对象,如下:而在Java中,如果两个字符串变量指向常量池中的同一个对象,则使用==比较操作符会返回true。Java编译器为什么会有这种优化呢?主要是为了性能考虑,因为"a"+"b"这个字符串的值,在编译时就已经确定了,且不会变化。因此,编译器可以在编译时就执行这些表达式的计算,而不是等到运行时。这样做的好处减少了运行时

    2024-03-14
    阅读(145)
  • 回答String的intern()是一个用于字符串添加到字符串常量池中,它的作用是:当调用intern()方法时,如果常量池中已经该字符串,则返回池中的字符串;如果常量池中不存在该字符串,则根据版本的不同处理方式不同:如果在JDK7以前,由于字符串常量池在永久代,intern()会将这个字符串的副本添加到常量池中,并返回常量池中的引用。从JDK7开始,字符串常量池被移到了Java堆内存中,则intern()则是将Java堆中的引用复制到字符串常量池中,而不是复制字符串副本,这样,intern()会返回指向堆上已存在对象的引用。intern()是基于字符串常量池(StringPool)。字符串常量池是Java堆中(JDK7以后)的一部分,其目的是存储字符串字面量和显式调用intern()方法的字符串实例,这样可以减少Java虚拟机中相同内容字符串对象的数量,以节省内存资源。注:JDK6以前,

    2024-03-14
    阅读(128)
  • 回答1个或者2个。1个:若字符串常量池中已经存在了"abc"这个值,则只会在Java堆上创建1个对象。2个:若字符串常量池中没有"abc"这个值,则会创建2个对象,一个是字符串常量池中的对象,一个是Java堆上的。详解字符串常量池是这篇文章的基础,关于字符串常量池请先阅读这篇文章:你是如何理解常量池的?我们先看这个代码:publicclassStringTest{publicstaticvoidmain(String[]args){Stringstr=newString("abc");}}我们先编译,然后利用javap-verboseStringTest查看结果:publicclasscom.skjava.demo.skjavademo.java.jichu.StringTestminorversion:0majorversion

    2024-03-14
    阅读(129)
  • 回答我们都知道String是不可变的,那它是如何来实现不可变的呢?主要依赖下面几个机制。私有不可变数组String底层使用一个私有的字符数组来存储字符串数据,同时这个数组被声明为final。这就意味着数组的引用是不可以被重新指向另一个数组,保证了String对象创建后其内部数组不能指向其他数据。privatefinalcharvalue[];注意,从Java9开始,String底层数组改用byte[]加上编码标记(coder),以优化性能和存储空间,但这不影响不可变性的基本原理。请参考:Java9为什么要改变String的底层数组?类声明为finalString类被声明为final,这就意味着String不允许被继承,这就阻止了任何想通过继承String并覆盖String的方法来改变String不可变的行为。保证了String的行为不会被任何子类修改。没有修改方法String没有提供任何方

    2024-03-14
    阅读(137)
  • 回答作为Java中使用最为广泛的String,它被设计为不可变,这就说明一旦一个String对象被创建,那么它就不能被更改了。这样的好处有如下几个:安全String经常被用于存储敏感信息,如网络连接的URL、用户名和密码,如果String设计为可变的,那么这些敏感信息就可能会被恶意篡改,增加安全风险。多线程安全String不可变,天然线程安全,因为具备不变性的对象一定是线程安全的。所以在多线程条件下,我们不需要采取任何额外的保护机制。更高的效率和性能String不可变使得实现字符串常量池成为可能。字符串常量池是JVM中一个特殊的存储区域,用于存储所有的字符串字面量和字符串常量表达式。这意味着每个唯一的字符串字面量在JVM中只有一个副本,当我们请求创建相同的String对象时就不用每次都创建一个新对象,减少了内存的消耗。如果String是可变的,字符串常量池的这种优化就无法实现了,因为字符串

    2024-03-14
    阅读(131)
  • 回答在Java中表示金额时,关键是要保证精确度,避免在运算过程中出现误差,一般来说我们有如下几种方案。使用int或long类型通过将金额表示为其最小货币单位(例如,分、厘等)的整数形式,比如10元可以存储为1000分,或者10000厘,具体表现形式看系统的最小货币单位。这种方式可以避免因为浮点运算而导致的精度问题,简单而又高效。使用BigDecimalBigDecimal是Java中专门用于高精度计算的类,它非常适合用来表示金额。使用它不仅可以避免浮点类型计算中的精度丢失问题,而且还可以控制四色五人模式,而且BigDecimal还提供了丰富的API可以用来进行数学运算。一般情况下,在使用BigDecimal初始化时,推荐使用字符串形式来创建,例如newBigDecimal("10.00")表示10元。但是,使用BigDecimal会存在一些坑,使用不当也是会出现问题的。

    2024-03-14
    阅读(130)
  • 回答这是因为BigDecimal的equals()不仅比较数值的大小,而且还比较数值的精度。这就意味着,即使两个BigDecimal对象代表相同的数值,但如果它们的精度不同,equals()方法会认为这两个对象不相等。详解@Testpublicvoidtest01(){BigDecimalbd1=newBigDecimal("2.00");BigDecimalbd2=newBigDecimal("2.0");System.out.println(bd1.equals(bd2));}//执行结果...false两个值数值一样,但是得到的结果却是false。我们看equals()的源码:在源码中我们发现,它会比较两个数值的精度。这两个值的精度是不一样的:在大多数情况下,特别是在处理金融数值时,我们其实只关心数值是否相等,而不关心它们的精度是否完全一样。在

    2024-03-14
    阅读(140)
  • 答案在处理金融计算和表示金额时,是不能使用浮点数的(如float和double),主要原因是涉及到浮点数的精度问题。在计算机中所有的数据都要转换为二进制才能被计算机处理,但很遗憾,很多十进制小数在转换为二进制表示时是无限循环小数,这就导致了精确度的损失。所以,浮点数只是近似值,并不是精确值,不能用来表示金额,因为会有精度丢失。扩展浮点精度丢失先看如下程序:@Testpublicvoidtest01(){floatf1=6.6f;floatf2=1.3f;System.out.println(f1+f2);}//执行结果7.8999996得到的答案并不是我们预想中的0.9。再看一个:@Testpublicvoidtest02(){floatsum=0;for(inti=0;i<10;i++){sum+=0.1;System.out.println(sum);}}执行结果:0.10.20

    2024-03-13
    阅读(166)
  • 回答接口和抽象类是Java中实现抽象的两种不同的方式。它们都不能被实例化,可以包含方法声明,但是在使用方式、目的和能力上有明显的不同。1、方法声明在Java8以前,接口是只能声明方法,不能实现方法。在Java8以后,接口可以有默认方法。抽象类可以有抽象方法(不提供方法实现)和具体方法(提供方法实现)。2、属性接口中属性默认是publicstaticfinal的。同时,接口中的修饰符必须是public,不允许使用其他的修饰符,如果声明了某个属性也必须要求初始化。抽象类中的属性对修饰符没有要求,可以是public、protected、private。3、继承与实现接口可以被实现,也可以被继承,同时一个类可以实现多个接口,一个接口也可以继承多个接口。抽象类只能被继承,且一个类只能继承一个抽象类。4、构造器接口没有构造器抽象类可以有构造器,但是它不支持实例化,它的构造器的意义在于完成属性的初始化操

    2024-03-13
    阅读(131)
  • 回答Java不支持多重继承的主要原因是为了避免复杂性和简化设计,特别是为了避免“菱形问题”。在多重继承的情况下,如果一个类继承自两个具有相同方法签名的父类,子类将不清楚应该继承哪个父类的方法,这会导致歧义和设计上的复杂性。虽然java不支持多重继承,但是我们可以通过使用接口或者接口的默认方法来间接实现类似“多重继承”的功能。扩展菱形问题菱形问题是多重继承可能引发的一个典型问题。假如有类B和C,他们继承A,然后又有一个类D通过多重继承的方式继承B和C,那么他们之间的继承关系就形成了一个菱形:假如A定义了一个doSomething(),B和C两个子类都重写了doSomething(),如果这个时候D调用doSomething(),那么编译器或在运行时就会面临一个问题:应该调用B的doSomething(),还是C的doSomething()?如果只有B重写了doSomething()方法,那么

    2024-03-13
    阅读(113)
  • 在Java中,无论其前面的try代码块是否抛出异常,finally都会执行到,所以它通常被用来关闭资源或进行清理操作。但是确实是存在一些特殊情况,导致finally是不会执行到的。详解1、在try语句之前return或者抛出异常@TestpublicvoidfinallyTest(){inti=1/0;try{//....}finally{System.out.println("执行finally");}}finally语句被执行的必要而非充分条件是相应的try语句一定被执行到。2、程序所在的线程死亡序所在的线程在进入finally代码块之前死亡,例如调用System.exit(0),finally代码块不会被执行。@TestpublicvoidfinallyTest(){try{System.out.println("执行try代码块");Syst

    2024-03-13
    阅读(140)
  • 这是一个开发规范的问题。在阿里巴巴Java的开发手册中有这么一段:为什么要加一个这样的强制呢?回答大明哥认为有如下几个原因。原因一:空值的表达能力包装类型默认null,而基本数据类型不能。这使得包装类型可以更加准确地表达出属性缺少值的意义。而这个能力在于数据库交互及对象之间的数据传递时尤其有用。比如,我们有一个考试系统,课程Course类里面有一个属性completed表示用户是否完成了课程:publicclassCourse{privateBooleancompleted;//使用包装类型Boolean//其他属性和方法}如果我们使用包装类,那么默认为空,可以表达这门课程的用户状态是未知的,这可能意味着学生尚未开始课程或系统尚未更新他们的进度。而如果使用基本类型,则默认为false,则表示用户尚未完成课程,容易产生歧义。原因二:避免默认值带来的歧义基本数据类型有默认值,如int的默认值为

    2024-03-13
    阅读(117)
  • 回答Java的自动拆装箱是Java5引入的一个特性,它允许Java自动地在基本类型和它们对应的包装类类型之间进行转换。转换过程是透明的,由编译器帮我们完成。该机制能简化我们的编程。详解装箱是指将基本数据类型转换为相应的包装类对象的过程:Integerinteger=10;自动装箱的底层实现依赖于编译器的转换机制,当我们把一个基本类型赋值给对应的包装类对象时,编译器会自动将这个赋值操作扩展为调用包装类的valueOf()方法,如上面代码,编译器会自动转换Integeri=Integer.valueOf(10);拆箱则是指将包装类对象转换为相应的基本数据类型值的过程:inti=integer;拆箱的底层实现也是依赖于编译器的转换机制,当我们从一个包装类对象读取基本类型的值时,编译器会自动将这个操作扩展为调用包装类对象的xxxValue()方法,其中xxx代表对应的基本类型,如上面代码,编译器会

    2024-03-13
    阅读(124)
  • 回答int是Java的8个原始数据类型之一,其默认值为0。Integer是int的包装类,它属于Java对象,它内部有一个int类型的value字段来存储数据,其初始值为null。既然Integer为Java对象,那么它除了存储数据本身外,还包含了一些其他的信息,比如对象的元数据,所以Integer比int更加占用内存。同时,Java5引入自动装箱和拆箱机制,允许int和Integer之间的自动转换,这样简化了编程模型,同时保留了基本数据类型的性能优势和引用类型的便利性。至于为什么要这样设计,其根本原因还是因为Java是面向对象的编程语言,在Java中一切皆是对象(基本类型除外),而且在Java中引用类型能够提供更加丰富的功能,比如Integer内部封装一些方法能够让我们更加方便的操作它们。而且,Java的泛型和集合只能使用引用类型,不能使用基本数据类型。Integer等包装类使得基本数据

    2024-03-09
    阅读(137)
  • 实例对象的创建,我详细小伙伴们一定熟悉得不能再熟悉了,那你知道Java中创建对象到底有多少种方法吗?这里大明哥将介绍6中方式来创建一个Java对象:使用new关键字使用Class类的newInstance()使用clone()方法使用Constructor类的newInstance()使用对象的反序列化使用ObjectFactory或其他工厂方法使用new关键字这是最常见也是最简单的创建对象的方式,当使用new关键字时,Java为对象分配内存,并调用类的构造函数初始化对象。Useruser=newUser("大明哥",18);这种方式是我们大多数标准对象创建的方式。使用Class类的newInstance()每个类都是Class类的实例,我们可以通过Class对象的newInstance()来创建类的实例,这通常是通过反射来实现的。Useruser=User.class

    2024-03-09
    阅读(96)
  • 回答这是因为equals()和hashCode()之间存在两个契约关系:如果两个对象相等(即equals()返回true),那么它们的hashCode()必须返回相同的整数值。如果两个对象的hashCode()返回相同的值,并不要求这两个对象一定相等(即equals()返回true),但如果两个对象的hashCode()返回不相同值,那要求这两个对象不相等(equals()返回false)。基于这种契约关系,如果我们通过重写equals()来改变对象的相等逻辑,但是却没有重写hashCode(),那么这两个方法的契约关系就被破坏了。破坏了这种契约关系会导致我们无法正常使用基于散列的集合类(如HashSet,HashMap,HashTable):在HashSet中,我们知道它是不能有重复数据的。如果equals()被重写以改变相等性判断,而hashCode()没有相应地被重写,那么即使两个对

    2024-03-09
    阅读(124)
  • 两个对象用==比较,比较的是两个对象的内存地址,而不是他们值,a1和a2粗看就是两个不同的对象,那么结果就是false,事实如何呢?我们写个程序运行下:Integera1=100;Integera2=100;System.out.println("a1==a2:"+(a1==a2));//结果...a1==a2:true为什么会是true呢?不是两个不同的对象么?这其实是跟Integer的实现机制有关。为了提供性能和减少内存的消耗,Java引入了一个IntegerCache机制,该机制用于缓存-128~127的Integer对象。Integer中有一个IntegerCache的内部类,它就是负责存储这个范围内的Integer实例的缓存对象。当我们使用Integer.valueOf(int)方法创建Integer,它首先会检查该值是否在缓存范围内,如果是,则返回缓存中的对

    2024-03-09
    阅读(153)
  • 在Java中,调用方法时,参数的传递分为两种类型:传递值传引用值如果是传递值类型,意味着方法接收的是实际值的一个副本,原始值不会被改变。例如:publicclassTest{publicstaticvoidmain(String[]args)throwsException{inti=10;System.out.println("原始值i="+i);add(i);System.out.println("调用add()后的i="+i);}privatestaticintadd(inti){i=i+10;System.out.println("add()中的i="+i);returni;}}执行结果:调用add()方法后,值i并没有发生改变,还是10。为什么呢?因为main()调用add()传递的是i的一个副本,我们可以简单理解为它是另

    2024-03-09
    阅读(139)