CrabNote螃蟹笔记

Java基础面试题

本文《Java基础面试题》由小宇宙从原站整理搬运,详细讲解相关技术要点。

大家好,我是小宇宙。 Java 基础这块,在面试里的地位有点特殊——它不像 MySQL、Redis 那样每次都是核心考点,但如果你答得不好,会给面试官留下"基本功不扎实"的印象,这个代价其实挺大的。很多人用 Java 写了好几年代码,却说不清楚 JVM 和 JDK 的关系、int 和 Integer 的区别、为什么要重写 hashCode,这些问题看起来简单,但答起来很容易露馅。 这篇文章整理了 Java 基础面试中最常被问到的知识点,涵盖核心概念、数据类型、面向对象、关键字、反射、注解、异常、Java 8 新特性、IO 等内容,覆盖比较全面。内容偏向理解,不是让你背定义,而是帮你搞清楚"这个东西在 Java 里为什么是这样设计的"。 有几块在面试里出现频率特别高,建议重点花时间: 面向对象三大特性 :封装、继承、多态,尤其是多态的几种体现形式、重载和重写的区别,这类题看起来基础,但很多人答得很浅,容易被追问到哑口无言。 int 和 Integer :为什么要有包装类、自动装箱拆箱的原理、Integer 的缓存机制( 128 到 127),这条线问得很频繁,而且很容易出坑。 String、StringBuffer、StringBuilder :三者的区别和适用场景,以及 String 不可变性的原理,这是一道老题,但每次还是会问。 equals 和 hashCode :为什么要配套重写、不重写会有什么问题,在 HashMap 和 HashSet 里的影响,这个很多人只知道结论,答不出背后的原因。 Java 8 新特性 :Lambda 表达式、Stream API、Optional,这些在实际开发里用得很多,面试里也经常问。 反射机制 :是什么、怎么用、实际应用在哪里(Spring 的 IOC 就是典型),这块理解清楚了对后面学框架原理很有帮助。 如果你是第一次系统准备 Java 基础,建议先把面向对象和数据类型搞清楚,再去看反射、注解这些稍微深一点的内容,整体理解起来会顺很多。 概念 说一下Java的特点 主要有以下的特点: 平台无关性 :Java的“编写一次,运行无处不在”哲学是其最大的特点之一。Java编译器将源代码编译成字节码(bytecode),该字节码可以在任何安装了Java虚拟机(JVM)的系统上运行。 面向对象 :Java是一门严格的面向对象编程语言,几乎一切都是对象。面向对象编程(OOP)特性使得代码更易于维护和重用,包括类(class)、对象(object)、继承(inheritance)、多态(polymorphism)、抽象(abstraction)和封装(encapsulation)。 内存管理 :Java有自己的垃圾回收机制,自动管理内存和回收不再使用的对象。这样,开发者不需要手动管理内存,从而减少内存泄漏和其他内存相关的问题。 Java 的优势和劣势是什么? 首先,Java的优势,我记得跨平台应该是一个大点,因为JVM的存在,一次编写到处运行。然后面向对象,这个可能也是优势,不过现在很多语言都支持面向对象,但是Java的设计从一开始就是OOP的。还有强大的生态系统,比如Spring框架,Hibernate,各种库和工具,社区支持大,企业应用广泛。另外,内存管理方面,自动垃圾回收机制,减少了内存泄漏的问题,对开发者友好。还有多线程支持,内置的线程机制,方便并发编程。安全性方面,Java有安全模型,比如沙箱机制,适合网络环境。还有稳定性,企业级应用长期使用,版本更新也比较注重向后兼容。 劣势的话,性能可能是一个,虽然JVM优化了很多,但相比C++或者Rust这种原生编译语言,还是有一定开销。特别是启动时间,比如微服务场景下,可能不如Go之类的快。语法繁琐,比如样板代码多,之前没有lambda的时候更麻烦,现在有了但比起Python还是不够简洁。内存消耗,JVM本身占内存,对于资源有限的环境可能不太友好。还有面向对象过于严格,有时候写简单程序反而麻烦,虽然Java8引入了函数式编程,但不如其他语言自然。还有开发效率,相比动态语言如Python,Java需要更多代码,编译过程也可能拖慢开发节奏。 Java为什么是跨平台的? Java 能支持跨平台,主要依赖于 JVM 关系比较大。 JVM也是一个软件,不同的平台有不同的版本。我们编写的Java源码,编译后会生成一种 .class 文件,称为字节码文件。Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码然后运行。也就是说,只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。 而这个过程中,我们编写的Java程序没有做任何改变,仅仅是通过JVM这一”中间层“,就能在不同平台上运行,真正实现了”一次编译,到处运行“的目的。 JVM是一个”桥梁“,是一个”中间件“,是实现跨平台的关键,Java代码首先被编译成字节码文件,再由JVM将字节码文件翻译成机器语言,从而达到运行Java程序的目的。 编译的结果不是生成机器码,而是生成字节码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。 所以,运行Java程序必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的再次翻译才能执行。即使你将Java程序打包成可执行文件(例如 .exe),仍然需要JVM的支持。 跨平台的是Java程序,不是JVM。JVM是用C/C++开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的JVM。 JVM、JDK、JRE三者关系? 它们之间的关系如下: JVM是Java虚拟机,是Java程序运行的环境。它负责将Java字节码(由Java编译器生成)解释或编译成机器码,并执行程序。JVM提供了内存管理、垃圾回收、安全性等功能,使得Java程序具备跨平台性。 JDK是Java开发工具包,是开发Java程序所需的工具集合。它包含了JVM、编译器(javac)、调试器(jdb)等开发工具,以及一系列的类库(如Java标准库和开发工具库)。JDK提供了开发、编译、调试和运行Java程序所需的全部工具和环境。 JRE是Java运行时环境,是Java程序运行所需的最小环境。它包含了JVM和一组Java类库,用于支持Java程序的执行。JRE不包含开发工具,只提供Java程序运行所需的运行环境。 JVM 和 Java 有啥区别? Java是语言,JVM是平台,一个是写代码的工具,一个是跑代码的环境,两者分工不同但相互配合。 简单来说,Java是一门编程语言,就是我们平时写代码用的那个语言,像String、List这些都是Java语言提供的。而JVM呢,全称是Java虚拟机,它是用来运行Java程序的一个平台或者说运行环境。 打个比方,Java就像是我们写文章用的中文,而JVM就像是能够阅读和理解这篇文章的人。我们用Java语言写代码,但代码不能直接运行,得先通过编译器编译成字节码,也就是.class文件,然后JVM再把这些字节码翻译成机器能懂的指令去执行。 这里面还有一个很关键的点,就是Java能够跨平台,也就是"一次编译,到处运行"。这个跨平台能力其实主要是JVM提供的。我们写好的Java代码编译成字节码后,这份字节码可以在Windows的JVM上运行,也可以在Linux的JVM上运行,还可以在Mac上运行。虽然底层操作系统不一样,但因为有JVM这一层,它帮我们屏蔽了这些差异,做好了适配。所以本质上是JVM依赖平台,但Java字节码不依赖平台。 再简单说说它们和其他概念的关系。JDK是开发工具包,里面有编译器、调试工具这些,也包含了JRE。JRE是运行环境,包含了JVM和Java的核心类库。所以关系就是JDK大于JRE大于JVM,一个比一个小。如果你要开发Java程序就得装JDK,如果只是运行别人写好的程序,装个JRE就够了。 还有一点,JVM其实不只能跑Java。因为JVM执行的是字节码,所以像Kotlin、Scala这些语言,虽然语法跟Java不一样,但编译后也是生成Java字节码,一样可以在JVM上运行。所以JVM现在已经不只是Java的专属了,而是整个JVM语言生态的基础平台。 为什么Java解释和编译都有? 首先在Java经过编译之后生成字节码文件,接下来进入JVM中,就有两个步骤编译和解释。 如下图: 编译 : Java 源代码会先被 编译为平台无关的字节码( )文件,这一步发生在程序运行之前。 解释 / JIT : JVM 启动后由解释器逐条解释执行字节码;同时 JVM 通过方法调用计数器和回边计数器识别热点代码,达到阈值后由 JIT 编译为本地机器码并缓存到 Code Cache 中,后续执行直接走机器码。 所以Java既是编译型也是解释性语言,默认采用的是解释器和编译器混合的模式。 jvm是什么 JVM是 java 虚拟机,主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。 JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是Java能够“ 一次编译,到处运行的 ”原因。 编译型语言和解释型语言的区别? 编译型语言和解释型语言的区别在于: 编译型语言:在程序执行之前,整个源代码会被编译成目标平台的机器码(注:Java 这种"编译为字节码再由虚拟机解释 / JIT 执行"的模式严格说属于编译型与解释型的混合),生成可执行文件。执行时直接运行编译后的代码,速度快,但跨平台性较差。 解释型语言:在程序执行时,逐行解释执行源代码,不生成独立的可执行文件。通常由解释器动态解释并执行代码,跨平台性好,但执行速度相对较慢。 典型的编译型语言如C、C++,典型的解释型语言如Python、JavaScript。 Python和Java区别是什么? Java 是一种编译与解释混合执行的语言:源代码先被 编译成字节码(.class),再由 JVM 加载后通过解释器 + JIT 即时编译器执行。 Python 是一种解释型语言,运行时由解释器逐行翻译并执行源代码(CPython 内部也会先编译为字节码 ,但整体执行模型仍以解释执行为主)。 值传递和引用传递的区别? 在 Java 中,参数传递只有 值传递 一种方式,不存在真正的 “引用传递”。但很多人会混淆这两个概念,核心区别在于传递的是 “值的副本” 还是 “引用的副本”。 值传递(Pass by Value)。 传递的是 实际值的副本 ,适用于 基本数据类型 (如 、 等),修改方法内的参数副本, 不会影响原变量的值 。例子: 引用传递的误解(本质仍是值传递)。 对于 对象(引用类型) ,传递的是 对象引用的副本 (而非对象本身)。 两个引用(原引用和副本)指向 同一个对象 ,因此通过副本修改对象内部数据, 会影响原对象 。但如果修改副本的指向(如重新赋值), 不会影响原引用的指向 。示例: 简单来说,Java 中 所有参数传递都是值传递 : 基本类型传递 “值的副本”,修改副本不影响原值。 引用类型传递 “引用的副本”,通过副本可修改对象内容,但无法改变原引用的指向。 数据类型 八种基本的数据类型 Java支持数据类型分为两类: 基本数据类型和引用数据类型。 基本数据类型共有8种,可以分为三类: 数值型:整数类型(byte、short、int、long)和浮点类型(float、double) 字符型:char 布尔型:boolean 8种基本数据类型的默认值、位数、取值范围,如下表所示: | 数据类型 | 占用大小(字节) | 位数 | 取值范围 | 默认值 | 描述 | | | | | | | | | | 1 | 8 | 128( 2^7) 到 127(2^7 1) | 0 | 是最小的整数类型,适合用于节省内存,例如在处理文件或网络流时存储小范围整数数据。 | | | 2 | 16 | 32768( 2^15) 到 32767(2^15 1) | 0 | 较少使用,通常用于在需要节省内存且数值范围在该区间的场景。 | | | 4 | 32 | 2147483648( 2^31) 到 2147483647(2^31 1) | 0 | 最常用的整数类型,可满足大多数日常编程中整数计算的需求。 | | | 8 | 64 | 9223372036854775808( 2^63) 到 9223372036854775807(2^63 1) | 0L | 用于表示非常大的整数,当 类型无法满足需求时使用,定义时数值后需加 或 。 | | | 4 | 32 | 3.4028235E38 到 3.4028235E38 | 0.0f | 单精度浮点数,用于表示小数,精度相对较低,定义时数值后需加 或 。 | | | 8 | 64 | 1.7976931348623157E308 到 1.7976931348623157E308 | 0.0d | 双精度浮点数,精度比 高,是 Java 中表示小数的默认类型。 | | | 2 | 16 | '\u0000'(0) 到 '\uffff'(65535) | '\u0000' | 用于表示单个字符,采用 Unicode 编码,可表示各种语言的字符。 | | | 无明确字节大小(理论上 1 位) | 无明确位数 | 或 | | 用于逻辑判断,只有两个取值,常用于条件判断和循环控制等逻辑场景。 | Float 和 Double 的最大值都是以科学记数法的形式输出的,结尾的"E+数字"表示 E 之前的数字要乘以 10 的多少倍。比如 3.14E3 就是 3.14×1000=3140,3.14E 3 就是 3.14/1000=0.00314。注意: / 表示的是 最小正非零值 (如 ),并不是负的最小值;浮点类型的最小负值是 。 注意一下几点: Java八种基本数据类型的字节数:1字节(byte、boolean)、 2字节(short、char)、4字节(int、float)、8字节(long、double) 浮点数的默认类型为double(如果需要声明一个常量为float型,则必须要在末尾加上f或F) 整数的默认类型为int(声明Long型在末尾加上l或者L) 八种基本数据类型的包装类:除了char的是Character、int类型的是Integer,其他都是首字母大写 char类型是无符号的,不能为负,所以是0开始的 int和long是多少位,多少字节的? 类型是 32 位(bit),占 4 个字节(byte),int 是有符号整数类型,其取值范围是从 2^31 到 2^31 1 。例如,在一个简单的计数器程序中,如果使用 类型来存储计数值,它可以表示的最大正数是 2,147,483,647。如果计数值超过这个范围,就会发生溢出,导致结果不符合预期。 类型是 64 位,占 8 个字节, 类型也是有符号整数类型,它的取值范围是从 2^63 到 2^63 1 ,在处理较大的整数数值时,果 类型的取值范围不够,就需要使用 类型。例如,在一个文件传输程序中,文件的大小可能会很大,使用 类型可能无法准确表示,而 类型就可以很好地处理这种情况。 long和int可以互转吗 ? 可以的,Java中的 和 可以相互转换。由于 类型的范围比 类型大,因此将 转换为 是安全的,而将 转换为 可能会导致数据丢失或溢出。 将 转换为 可以通过直接赋值或强制类型转换来实现。例如: 将 转换为 需要使用强制类型转换,但需要注意潜在的数据丢失或溢出问题。 例如: 在将 转换为 时,如果 的值超出了 类型的范围,转换结果将是截断后的低位部分。因此,在进行转换之前,建议先检查 的值是否在 类型的范围内,以避免数据丢失或溢出的问题。 数据类型转换方式你知道哪些? 自动类型转换(隐式转换):当目标类型的范围大于源类型时,Java会自动将源类型转换为目标类型,不需要显式的类型转换。例如,将 转换为 、将 转换为 等。 强制类型转换(显式转换):当目标类型的范围小于源类型时,需要使用强制类型转换将源类型转换为目标类型。这可能导致数据丢失或溢出。例如,将 转换为 、将 转换为 等。语法为:目标类型 变量名 = (目标类型) 源类型。 字符串转换:Java提供了将字符串表示的数据转换为其他类型数据的方法。例如,将字符串转换为整型 ,可以使用 方法;将字符串转换为浮点型 ,可以使用 方法等。 数值之间的转换:Java提供了一些数值类型之间的转换方法,如将整型转换为字符型、将字符型转换为整型等。这些转换方式可以通过类型的包装类来实现,例如 类、 类等提供了相应的转换方法。 类型互转会出现什么问题吗? 基本数据类型转换的问题 当把小范围数据类型赋值给大范围数据类型时,Java 会自动进行类型转换,这种转换一般是安全的。 但是大范围数据类型赋值给小范围数据类型时,会发生数据数据溢出或者精度损失的问题。 数据溢出:如果大范围数据类型赋值给小范围数据类型时,当目标类型无法容纳原数据时,就会发生数据溢出。比如下面,byte 类型的取值范围是 128 到 127。300 的二进制表示为 ,强制转换为 byte 类型时,会丢弃高位字节,只保留低位的 8 位 ,其十进制值为 44。 精度损失:在进行浮点数类型的转换时,可能会发生精度损失。由于浮点数的表示方式不同,将一个单精度浮点数( )转换为双精度浮点数( )时,精度可能会损失,如果 double 转换为 int 也会发生精度损失的问题,如下: 对象引用转换的问题 向上转型是自动进行的,而且是安全的,如下: 但是向下转型需要手动进行,并且存在风险。如果父类对象实际上并不是目标子类的实例,在转型时就会抛出异常: 原因是Java 的对象在运行时会记录其真实类型,当进行向下转型时,Java 会检查对象的实际类型是否与目标类型兼容。如果不兼容,就会抛出 。 解决方式是需要使用 检查: 为什么用bigDecimal 不用double ? double会出现精度丢失的问题,double执行的是二进制浮点运算,二进制有些情况下不能准确的表示一个小数,就像十进制不能准确的表示1/3(1/3=0.3333...),也就是说二进制表示小数的时候只能够表示能够用1/(2^n)的和的任意组合,但是0.1不能够精确表示,因为它不能够表示成为1/(2^n)的和的形式。 比如: 可以看到在Java中进行浮点数运算的时候,会出现丢失精度的问题。那么我们如果在进行商品价格计算的时候,就会出现问题。很有可能造成我们手中有0.06元,却无法购买一个0.05元和一个0.01元的商品。因为如上所示,他们两个的总和为0.060000000000000005。这无疑是一个很严重的问题,尤其是当电商网站的并发量上去的时候,出现的问题将是巨大的。可能会导致无法下单,或者对账出现问题。 而 Decimal 是精确计算 , 所以一般牵扯到金钱的计算 , 都使用 Decimal。 在上述代码中,我们创建了两个 对象 和 ,分别表示0.1和0.2这两个十进制数。然后,我们使用 方法计算它们的和,并使用 方法计算它们的乘积。最后,我们通过 打印结果。 这样的使用 可以确保精确的十进制数值计算,避免了使用 可能出现的舍入误差。需要注意的是,在创建 对象时,应该使用字符串作为参数,而不是直接使用浮点数值,以避免浮点数精度丢失。 装箱和拆箱是什么? 装箱(Boxing)和拆箱(Unboxing)是将基本数据类型和对应的包装类之间进行转换的过程。 自动装箱主要发生在两种情况,一种是赋值时,另一种是在方法调用的时候。 赋值时 这是最常见的一种情况,在Java 1.5以前我们需要手动地进行转换才行,而现在所有的转换都是由编译器来完成。 方法调用时 当我们在方法调用时,我们可以传入原始数据值或者对象,同样编译器会帮我们进行转换。 show方法接受Integer对象作为参数,当调用 时,会将int值转换成对应的Integer对象,这就是所谓的自动装箱,show方法返回Integer对象,而 中result为int类型,所以这时候发生自动拆箱操作,将show方法的返回的Integer对象转换成int值。 自动装箱的弊端 自动装箱有一个问题,那就是在一个循环中进行自动装箱操作的情况,如下面的例子就会创建多余的对象,影响程序的性能。 上面的代码 可以看成 ,但是 这个操作符不适用于Integer对象,首先sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下 由于我们这里声明的 sum 为 Integer 类型,自动装箱实际上由编译器替换为 调用,命中 (默认 128 127)时会复用缓存对象,但本例中循环值都已超出缓存范围,因此会创建将近 4000 个 Integer 对象,降低程序性能并加重 GC 负担。因此在编程时需要注意:正确声明变量类型,避免因为自动装箱引起的性能问题(另外, 自 JDK 9 起已被 ,应统一使用 或直接自动装箱)。 Java为什么要有Integer? Integer对应是int类型的包装类,就是把int类型包装成Object对象,对象封装有很多好处,可以把属性也就是数据跟处理这些数据的方法结合在一起,比如Integer就有parseInt()等方法来专门处理int型相关的数据。 另一个非常重要的原因就是在Java中绝大部分方法或类都是用来处理类类型对象的,如ArrayList集合类就只能以类作为他的存储对象,而这时如果想把一个int型的数据存入list是不可能的,必须把它包装成类,也就是Integer才能被List所接受。所以Integer的存在是很必要的。 泛型中的应用 在Java中,泛型只能使用引用类型,而不能使用基本类型。因此,如果要在泛型中使用int类型,必须使用Integer包装类。例如,假设我们有一个列表,我们想要将其元素排序,并将排序结果存储在一个新的列表中。如果我们使用基本数据类型int,无法直接使用Collections.sort()方法。但是,如果我们使用Integer包装类,我们就可以轻松地使用Collections.sort()方法。 转换中的应用 在Java中,基本类型和引用类型不能直接进行转换,必须使用包装类来实现。例如,将一个int类型的值转换为String类型,必须首先将其转换为Integer类型,然后再转换为String类型。 集合中的应用 Java集合中只能存储对象,而不能存储基本数据类型。因此,如果要将int类型的数据存储在集合中,必须使用Integer包装类。例如,假设我们有一个列表,我们想要计算列表中所有元素的和。如果我们使用基本数据类型int,我们需要使用一个循环来遍历列表,并将每个元素相加。但是,如果我们使用Integer包装类,我们可以直接使用stream()方法来计算所有元素的和。 Integer相比int有什么优点? int是Java中的原始数据类型,而Integer是int的包装类。 Integer和 int 的区别: 基本类型和引用类型:首先,int是一种基本数据类型,而Integer是一种引用类型。基本数据类型是Java中最基本的数据类型,它们是预定义的,不需要实例化就可以使用。而引用类型则需要通过实例化对象来使用。这意味着,使用int来存储一个整数时,不需要任何额外的内存分配,而使用Integer时,必须为对象分配内存。在性能方面,基本数据类型的操作通常比相应的引用类型快。 自动装箱和拆箱:其次,Integer作为int的包装类,它可以实现自动装箱和拆箱。自动装箱是指将基本类型转化为相应的包装类类型,而自动拆箱则是将包装类类型转化为相应的基本类型。这使得Java程序员更加方便地进行数据类型转换。例如,当我们需要将int类型的值赋给Integer变量时,Java可以自动地将int类型转换为Integer类型。同样地,当我们需要将Integer类型的值赋给int变量时,Java可以自动地将Integer类型转换为int类型。 空指针异常:另外,int 变量默认值为 0,而 Integer 作为引用类型默认值为 null。如果对一个值为 null 的 Integer(如未显式赋值的成员变量)执行自动拆箱操作,就会抛出空指针异常,因为 null 无法被拆箱为基本类型。 那为什么还要保留int类型? 包装类是引用类型,对象的引用和对象本身是分开存储的,而对于基本类型数据,变量对应的内存块直接存储数据本身。 因此,基本类型数据在读写效率方面,要比包装类高效。除此之外,在64位JVM上,在开启引用压缩的情况下,一个Integer对象占用16个字节的内存空间,而一个int类型数据只占用4字节的内存空间,前者对空间的占用是后者的4倍。 也就是说,不管是读写效率,还是存储效率,基本类型都比包装类高效。 说一下 integer的缓存 Java的Integer类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的Integer对象。 默认情况下,这个范围是 128至127。当通过Integer.valueOf(int)方法创建一个在这个范围内的整数对象时,并不会每次都生成新的对象实例,而是复用缓存中的现有对象,会直接从内存中取出,不需要新建一个对象。 面向对象 怎么理解面向对象?简单说说封装继承多态 面向对象是一种编程范式,它 将现实世界中的事物抽象为对象 ,对象具有属性(称为字段或属性)和行为(称为方法)。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的功能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。 Java面向对象的三大特性包括: 封装、继承、多态 : 封装 :封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。封装的目的是增强安全性和简化编程,使得对象更加独立。 继承 :继承是一种可以使得子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。 多态 :多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。 多态体现在哪几个方面? 多态在面向对象编程中可以体现在以下几个方面: 方法重载: 方法重载是指同一类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同)。虽然方法名相同,但根据传入的参数不同,编译器会在编译时确定调用哪个方法。 示例:对于一个 方法,可以定义为 和 。 方法重写: 方法重写是指子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的实际类型确定调用哪个版本的方法。这是实现多态的主要方式。 示例:在一个动物类中,定义一个 方法,子类 可以重写该方法以实现 ,而 可以实现 。 接口与实现: 多态也体现在接口的使用上,多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法。这使得程序在面对不同具体实现时保持一贯的调用方式。 示例:多个类(如 , )都实现了一个 接口,当用 类型的引用来调用 方法时,会触发对应的实现。 向上转型和向下转型: 在Java中,可以使用父类类型的引用指向子类对象,这是向上转型。通过这种方式,可以在运行时期采用不同的子类实现。 向下转型是将父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免 。 多态解决了什么问题? 多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类。 多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if else 语句等等 面向对象的设计原则你知道有哪些吗 面向对象编程中的六大原则: 单一职责原则(SRP) :一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。例子:考虑一个员工类,它应该只负责管理员工信息,而不应负责其他无关工作。 开放封闭原则(OCP) :软件实体应该对扩展开放,对修改封闭。例子:通过制定接口来实现这一原则,比如定义一个图形类,然后让不同类型的图形继承这个类,而不需要修改图形类本身。 里氏替换原则(LSP) :子类对象应该能够替换掉所有父类对象。例子:一个正方形是一个矩形,但如果修改一个矩形的高度和宽度时,正方形的行为应该如何改变就是一个违反里氏替换原则的例子。 接口隔离原则(ISP) :客户端不应该依赖那些它不需要的接口,即接口应该小而专。例子:通过接口抽象层来实现底层和高层模块之间的解耦,比如使用依赖注入。 依赖倒置原则(DIP) :高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。例子:如果一个公司类包含部门类,应该考虑使用合成/聚合关系,而不是将公司类继承自部门类。 最少知识原则 (Law of Demeter) :一个对象应当对其他对象有最少的了解,只与其直接的朋友交互。 重载与重写有什么区别? 重载(Overloading)指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表(参数类型、参数个数或参数顺序不同),编译器根据调用时的参数类型来决定调用哪个方法。 重写(Overriding)指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过@override注解来明确表示这是对父类方法的重写。 重载是指在同一个类中定义多个同名方法,而重写是指子类重新定义父类中的方法。 抽象类和普通类区别? 实例化:普通类可以直接实例化对象,而抽象类不能被实例化,只能被继承。 方法实现:普通类中的方法可以有具体的实现,而抽象类中的方法可以有实现也可以没有实现。 继承:普通类和抽象类在继承规则上完全一样——都只能被单继承( ),都可以实现多个接口( ),这一点两者没有区别。 实现限制:普通类可以被其他类继承和使用,而抽象类一般用于作为基类,被其他类继承和扩展使用。 Java抽象类和接口的区别是什么? 两者的特点: 抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。 接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8 以后可以有默认方法和静态方法)。适用于定义类的能力或功能。 两者的区别: 实现方式:实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。 方法方式:接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。 访问修饰符:接口成员变量默认为 ,必须赋初值,不能被修改;接口中的抽象方法默认是 ,从 Java 8 起接口可以定义 和 方法(带方法体),从 Java 9 起还可以定义 方法用于辅助 default 方法的实现。抽象类中成员变量默认为 default 访问权限,可在子类中被重新定义,也可被重新赋值;抽象方法被 abstract 修饰,不能被 private、static、synchronized 和 native 等修饰,必须以分号结尾,不带花括号。 变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态常量)。 抽象类能加final修饰吗? 不能 ,Java中的抽象类是用来被继承的,而final修饰符用于禁止类被继承或方法被重写,因此,抽象类和final修饰符是互斥的,不能同时使用。 接口里面可以定义哪些方法? 抽象方法 抽象方法是接口的核心部分,所有实现接口的类都必须实现这些方法。抽象方法默认是 public 和 abstract,这些修饰符可以省略。 默认方法 默认方法是在 Java 8 中引入的,允许接口提供具体实现。实现类可以选择重写默认方法。 静态方法 静态方法也是在 Java 8 中引入的,它们属于接口本身,可以通过接口名直接调用,而不需要实现类的对象。 私有方法 私有方法是在 Java 9 中引入的,用于在接口中为默认方法或其他私有方法提供辅助功能。这些方法不能被实现类访问,只能在接口内部使用。 抽象类可以被实例化吗? 在Java中,抽象类本身不能被实例化。 这意味着不能使用 关键字直接创建一个抽象类的对象。抽象类的存在主要是为了被继承,它通常包含一个或多个抽象方法(由 关键字修饰且无方法体的方法),这些方法需要在子类中被实现。 抽象类可以有构造器,这些构造器在子类实例化时会被调用,以便进行必要的初始化工作。然而,这个过程并不是直接实例化抽象类,而是创建了子类的实例,间接地使用了抽象类的构造器。 例如: 在这个例子中, 继承了 并实现了抽象方法 。当我们创建 的实例时, 的构造器被调用,但这并不意味着 被实例化;实际上,我们创建的是 的一个对象。 简而言之,抽象类不能直接实例化,但通过继承抽象类并实现所有抽象方法的子类是可以被实例化的。 接口可以包含构造函数吗? 在接口中,不可以有构造方法,在接口里写入构造方法时,编译器提示:Interfaces cannot have constructors,因为接口不会有自己的实例的,所以不需要有构造函数。 为什么呢?构造函数就是初始化class的属性或者方法,在new的一瞬间自动调用,那么问题来了Java的接口,都不能new 那么要构造函数干嘛呢?根本就没法调用 解释Java中的静态变量和静态方法 在Java中,静态变量和静态方法是与类本身关联的,而不是与类的实例(对象)关联。它们在内存中只存在一份,可以被类的所有实例共享。 静态变量 静态变量(也称为类变量)是在类中使用 关键字声明的变量。它们属于类而不是任何具体的对象。主要的特点: 共享性 :所有该类的实例共享同一个静态变量。如果一个实例修改了静态变量的值,其他实例也会看到这个更改。 初始化 :静态变量在类被加载时初始化,只会对其进行一次分配内存。 访问方式 :静态变量可以直接通过类名访问,也可以通过实例访问,但推荐使用类名。 示例: 静态方法 静态方法是在类中使用 关键字声明的方法。类似于静态变量,静态方法也属于类,而不是任何具体的对象。主要的特点: 无实例依赖 :静态方法可以在没有创建类实例的情况下调用。对于静态方法来说,不能直接访问非静态的成员变量或方法,因为静态方法没有上下文的实例。 访问静态成员 :静态方法可以直接调用其他静态变量和静态方法,但不能直接访问非静态成员。 多态性 :静态方法不支持重写(Override),但可以被隐藏(Hide)。 使用场景 静态变量 :常用于需要在所有对象间共享的数据,如计数器、常量等。 静态方法 :常用于助手方法(utility methods)、获取类级别的信息或者是没有依赖于实例的数据处理。 非静态内部类和静态内部类的区别? 区别包括: 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。 非静态内部类可以直接访问外部类的所有成员(包括实例变量和方法);静态内部类可以直接访问外部类的静态成员,访问外部类的实例成员则必须通过外部类的实例引用。 非静态内部类不能定义静态成员(Java 16 之前),而静态内部类可以定义静态成员。 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。 静态内部类和非静态内部类都 可以 访问外部类的私有成员(因为编译器会为嵌套类提供访问权限)——两者的区别在于静态内部类访问外部类的私有 实例 成员时必须先拿到外部类实例,而不是像非静态内部类那样能直接访问。 非静态内部类可以直接访问外部方法,编译器是怎么做到的? 非静态内部类可以直接访问外部方