Java特点与常识
3. 为什么说 Java 语言“编译与解释并存”?
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点
.class——>字节码:有些方法和代码经常被调用,引入JIT编译器(运行时编译),当JIT完成第一次编译后,会将字节码对应的机器码保存下来,下次可直接使用,因为机器码效率>java解释器
HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。
4. AOT 有什么优点?有什么缺点?为什么不全部使用 AOT 呢?
AOT 有什么优点?
JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation) 。和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。并且,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。
JIT 与 AOT 两者的关键指标对比:
可以看出,AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力,可以降低请求的最大延迟。
提到 AOT 就不得不提 GraalVMopen in new window 了!GraalVM 是一种高性能的 JDK(完整的 JDK 发行版本),它可以运行 Java 和其他 JVM 语言,以及 JavaScript、Python 等非 JVM 语言。 GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。
官方文档: https://www.graalvm.org/latest/docs/open in new window
AOT 有什么缺点?为什么不全部使用这种编译方式呢?
JIT 与 AOT,两者各有优点,只能说 AOT 更适合当下的云原生场景,对微服务架构的支持也比较友好。除此之外,AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。然而,很多框架和库(如 Spring、CGLIB)都用到了这些特性。如果只使用 AOT 编译,那就没办法使用这些框架和库了,或者说需要针对性地去做适配和优化。
举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 .class 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。
GraalVM 是一种高性能的 JDK
基本语法
关键字
36.default关键字的理解
这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制
- 在程序控制中,当在 switch 中匹配不到任何情况时,可以使用 default 来编写默认匹配的情况。
- 在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用 default 关键字来定义⼀个方法的默认实现。
- 在访问控制中,如果⼀个方法前没有任何修饰符,则默认会有⼀个修饰符 default ,但是加上了就会报错
自增自减运算符
符号在前就先加/减,符号在后就后加/减
移位运算符
- << :左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
- >> :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
- >>> :无符号右移,忽略符号位,空位都以 0 补齐。由于
- double,float 在二进制中的表现比较特殊,因此不能来进行移位操作。
- 移位操作符实际上支持的类型只有int和long,编译器在对short、byte、char类型进行移位前,都会将其转换为int类型再操作。
当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作
也就是说左移/右移 32 位相当于不进行移位操作(32%32=0)
long 对应的二进制是 64 位,因此求余操作的基数也变成了 64
16.break, continue, return的区别和作用?
- break: 跳出整个循环体,继续执行循环下面的语句
- continue: 跳出当前的这一次循环,继续下一次循环
- return: 用于跳出所在方法,结束该方法的运行
基本数据类型
3.Java 支持的数据类型有哪些?
- 原始数据类型(Primitive Data Types):
- 整数类型:byte、short、int、long
- 浮点数类型:float、double
- 字符类型:char
- 布尔类型:boolean
- 引用数据类型(Reference Data Types):
- 类(Class)
- 接口(Interface)
- 数组(Array)
- 字符串(String)
- 枚举(Enumeration)
- 自定义的类和接口等
像 byte、short、int、long能表示的最大正数都减 1 了。这是为什么呢?这是因为在二进制补码表示法中,最高位是用来表示符号的(0 表示正数,1 表示负数),其余位表示数值部分。所以,如果我们要表示最大的正数,我们需要把除了最高位之外的所有位都设为 1。如果我们再加 1,就会导致溢出,变成一个负数。
38.为什么浮点数运算的时候会有精度丢失的风险?如何解决浮点数运算的精度丢失问题?
- 这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示⼀个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况
// 0.2 转换为⼆进制数的过程为,不断乘以 2,直到不存在⼩数为⽌,
// 在这个计算过程中,得到的整数部分从上到下排列就是⼆进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发⽣循环)
...
- 大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
39.超过 long 整型的数据应该如何表示?
- BigInteger 内部使用 int[] 数组来存储任意大小的整形数据
- 相对于常规整数类型的运算来说 BigInteger 运算的效率会相对低
基本类型和包装类型的区别
- 用途: 除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
- 存储方式: 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
- 占用空间: 相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
- 默认值: 成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
- 比较方式: 对于基本数据类型来说,= = 比较的是值。对于包装数据类型来说,= = 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。
为什么说是几乎所有对象实例都存在于堆中呢?
这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存
基本数据类型存放在栈中是一个常见的误区!
基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中。
包装类型的缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。
Integer i1 = 40; Integer i2 = new Integer(40);
Integer i1=40 会发生装箱等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。
所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
4.什么是自动装箱和拆箱?
自动装箱(Autoboxing)和拆箱(Unboxing)是Java中用于原始数据类型和对应包装类之间的自动转换的机制。
从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。
-
自动装箱(Autoboxing): 原始数据类型——>包装类对象
int num = 10; Integer obj = num; // 自动装箱
-
自动拆箱(Unboxing): 包装类对象——>原始数据类型
Integer obj = 20; int num = obj; // 自动拆箱
注意: 如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作
5.自动拆箱引发的 NPE 问题
一个 null 值调用了 xxxValue()方法,当然会有 NPE 的问题了
类似的
6.为什么要有包装类型?包装类型的常量池技术?
1.除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。
我举个例子,假如你有一个对象中的属性使用基本类型,那这个属性就必然存在默认值了。这个逻辑不正确的!因为很多业务场景下,对象的某些属性没有赋值,我就希望它的值为 null。你给我默认赋个值,不是帮倒忙么?
另外,像泛型参数不能是基本类型。因为基本类型不是 Object 子类,应该用基本类型对应的包装类型代替。我们直接拿 JDK 中线程的代码举例。
Java 中的集合在定义类型的时候不能使用基本类型的。比如:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
Map<Integer, Set<String>> map = new HashMap<>();
2.Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False
变量
25.成员变量与局部变量的区别
- 定义位置:
- 成员变量:定义在类的内部,方法外部,作为类的成员。可以在类的任何方法、构造函数和代码块中使用。
- 局部变量:定义在方法、构造函数或代码块内部,作为方法或代码块的局部变量使用。
- 作用域:
- 成员变量:作用域为整个类,可以被类中的所有方法和代码块访问。
- 局部变量:作用域仅限于定义它的方法、构造函数或代码块,只能在其定义范围内使用。
- 访问修饰符:
- 成员变量:可以使用不同的访问修饰符(如public、protected、private)来控制对成员变量的访问权限。
- 局部变量:不可使用访问修饰符及static修饰,只在定义范围内可见。
- 但都能被 final 所修饰
- 默认值:
- 成员变量:如果未显式初始化,成员变量会有一个默认值。基本数据类型的默认值是0或false,引用类型的默认值是null。
- 局部变量:不会自动获得默认值,必须在使用之前显式初始化。
- 生命周期:
- 成员变量:与对象的生命周期相同
- 局部变量:与方法、构造函数或代码块的执行周期相同。
- 内存分配:
- 成员变量:存储在堆内存中,每个对象都有自己的成员变量副本。
- 局部变量:存储在栈内存中,当方法执行结束后,局部变量的内存会被自动释放。
27.静态变量和实例变量的区别?
- 定义位置: 静态变量是在类级别上定义的,通常位于类的顶部,而实例变量是在类中的方法外部定义的,属于对象级别。
- 存储方式: 静态变量存储在静态存储区中,而实例变量存储在每个对象的堆内存中。
- 内存分配: 静态变量在程序启动时就会被分配内存,并且只会有一份拷贝,无论创建多少个对象,它们都共享相同的静态变量。而实例变量在每个对象创建时都会分配内存,每个对象都有自己的实例变量副本。
- 生命周期: 静态变量的生命周期与程序的执行周期相同,它们在类被加载时创建,在整个程序的执行过程中都存在。而实例变量的生命周期与对象的生命周期相同,只有在对象存在时才有效。
- 访问方式: 静态变量可以直接通过类名访问,无需创建对象。静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员。无论⼀个类创建了多少个对象,它们都共享同⼀份静态变量。通常情况下,静态变量会被 final 关键字修饰成为常量
- 共享性:静态变量是类级别的变量,它们可以被所有对象共享。这意味着如果一个对象修改了静态变量的值,其他对象也会受到影响。而实例变量是每个对象独立拥有的,每个对象对实例变量的修改不会影响其他对象。
- 初始化时机: 静态变量在类加载时被初始化,而实例变量在对象创建时被初始化。静态变量的初始化可以在定义时直接赋值,或者通过静态代码块进行初始化。实例变量通常在类的构造方法中初始化,或者在对象创建后通过对象引用进行赋值。
28.静态变量和普通变量的区别?
- 生命周期:
- 静态变量:静态变量在程序执行期间始终存在,从程序启动到结束都存在于内存中。它们在内存中的位置是固定的,不会因为方法或函数的调用而改变。静态变量的值在声明时初始化,并且在程序的整个生命周期内保持不变。
- 普通变量:普通变量的生命周期取决于它们所在的作用域。当一个方法或函数执行完毕后,普通变量就会被销毁。每次调用该方法或函数时,都会重新创建并初始化普通变量。
- 共享性:
- 静态变量:静态变量被所有属于同一类或对象的实例共享。这意味着无论创建多少个类或对象的实例,它们都共享相同的静态变量。
- 普通变量:普通变量是每个类或对象的独立实例拥有的,它们在不同的实例之间是相互独立的。
- 访问方式:
- 静态变量:静态变量可以直接通过类名访问,无需创建类的实例。在类的方法内部,可以使用静态变量的名称来访问它们。
- 普通变量:普通变量需要通过类的实例来访问,即需要先创建一个对象,然后使用对象来访问实例变量。
- 内存分配:
- 静态变量:静态变量在程序启动时就会被分配内存空间,并且一直存在于内存中,直到程序结束才会释放。静态变量通常位于静态存储区域。
- 普通变量:普通变量在每次创建对象时才会被分配内存空间,并且在对象销毁时释放。普通变量通常位于堆或栈区域,取决于其作用域和声明方式。
方法
26.构造方法有哪些特性?
构造方法(Constructor)是在创建对象时被调用的特殊方法,用于初始化对象的状态。下面是构造方法的一些特性:
- 方法名与类名相同:构造方法的方法名必须与所在类的名称完全相同,包括大小写。
- 没有返回类型:构造方法没有返回类型,包括 void,因为构造方法的主要目的是初始化对象,而不是返回值。
- 自动调用:构造方法在创建对象时自动调用,无需手动调用。当使用关键字
new
创建类的实例时,相应的构造方法将被自动调用。 - 可以重载:与普通方法一样,构造方法可以进行重载,即在同一个类中可以定义多个构造方法,只要它们的参数列表不同即可。重载构造方法可以提供不同的初始化方式,以满足不同的对象创建需求,不能重写。
- 初始化对象的成员变量:构造方法常用于初始化对象的成员变量,可以在构造方法中为对象的属性赋初始值。
- 默认构造方法:如果一个类没有显式定义构造方法,Java 会提供一个默认的无参构造方法,用于创建对象。默认构造方法没有参数,仅执行默认的对象初始化操作。
- 可以有访问修饰符:构造方法可以使用访问修饰符来限制其可见性,例如 public、protected、private 等。
构造方法在面向对象编程中起着重要的作用,它们确保对象在创建时处于有效的状态,并提供了灵活的初始化机制。
29.静态方法和实例方法有何不同?
静态方法(Static Methods)和实例方法(Instance Methods)是面向对象编程中的两种不同类型的方法,它们有以下区别:
- 调用方式:
- 静态方法:静态方法可以直接通过类名调用,无需创建类的实例。在类的内部和外部,都可以使用类名来调用静态方法。
- 实例方法:实例方法需要通过类的实例来调用。首先需要创建一个对象,然后使用对象来调用实例方法。
- 访问权限:
- 静态方法:静态方法只能直接访问静态成员(静态变量和其他静态方法),不能直接访问实例成员(实例变量和实例方法)。这是因为静态方法在对象实例化之前就存在,无法访问尚未创建的实例变量和实例方法。
- 实例方法:实例方法可以直接访问实例成员和静态成员,包括实例变量、实例方法和静态成员。
- 关联对象:
- 静态方法:静态方法与类本身相关联,而不是与类的实例相关联。它们不依赖于任何特定的对象实例,因此无法使用关键字 “this” 来引用当前对象。
- 实例方法:实例方法与特定的对象实例相关联。在实例方法中,关键字 “this” 可以用来引用当前对象,从而访问实例成员。
- 功能用途:
- 静态方法:静态方法通常用于执行与类相关的操作,这些操作不依赖于特定的对象实例。它们可以用作工具方法、辅助方法或公共方法。
- 实例方法:实例方法通常用于操作和访问对象的状态,它们可以访问和修改实例变量,执行对象特定的行为。
30.在一个静态方法内调用一个非静态成员为什么是非法的?
在一个静态方法内调用一个非静态成员是非法的,因为静态方法不依赖于对象实例的存在,无法直接访问实例成员(非静态成员)。
它们在对象实例化之前就存在,并且可以通过类名直接调用。而实例成员(非静态成员)是与类的对象实例相关联的,需要通过对象实例才能访问。
33.重写和重载的区别?
重写(Override):
- 重写是指在子类中定义一个与父类中具有相同名称、参数列表和返回类型的方法,并且子类方法的访问修饰符不能更严格(即不能比父类方法更私有)。
- 重写用于实现多态性,即子类可以通过重写父类的方法来改变方法的实现细节,但方法的签名(名称、参数列表、返回类型)保持不变。
- 重写只能发生在继承关系中,子类重写父类的方法时必须保持方法签名的一致性,以确保正确的方法覆盖。
重载(Overload):
- 重载是指在同一个类中定义多个具有相同名称但参数列表不同的方法,参数列表可以有不同的类型、个数或顺序。
- 重载用于提供多个具有相似功能但参数略有不同的方法,方便调用者根据传递的参数来选择合适的方法进行调用。
- 重载可以发生在同一个类中,方法的名称相同但参数列表不同即可构成方法的重载。
7.什么是Java程序的主类?应用程序和小程序的主类有何不同?
- 主类是Java程序中的入口点,它包含程序的主要执行逻辑。在Java中,主类是指具有特定签名的特殊类。主类的定义如下:
public class Main {
public static void main(String[] args) {
// 主类的执行逻辑
}
}
- 在上述示例中,
Main
是主类,它包含一个静态的main
方法作为程序的入口点。main
方法是程序的起点,当程序运行时,JVM会首先执行main
方法中的代码。 - 应用程序的主类是指整个独立可运行的Java应用程序的入口点,它可以包含其他类和方法,用于实现程序的功能。应用程序的主类负责协调整个程序的执行流程和功能模块。
- 小程序的主类是指在Java平台上运行的较小、简单的程序,通常是一个单独的Java类,只包含一个
main
方法,用于执行特定的功能。小程序通常用于学习、演示或简单的任务。与应用程序不同,小程序的主类往往只关注单一的功能或任务,不涉及复杂的模块和架构。
8.说一说Object?
hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
9.Java和C++的区别?
向上和向下转型
10.访问修饰符public, private, protected和不写的区别?
public
(公共):对所有类和对象可见。private
(私有):仅对定义它们的类内部可见。protected
(受保护):对定义它们的类和派生类(继承)可见。- 不写修饰符:对同一包中的其他类可见,对不同包中的类不可见。
11.final有什么用?
- 用于修饰类、属性和方法
- 被final修饰的类不可以被继承
- 被final修饰的方法不可以被重写
- 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
12.final, finally, finalize的区别?
final
可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally
一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。(System代码强制停止除外)
finalize
是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
13.this和super的区别?
- "this"关键字用于在类的实例方法中引用当前对象,用于访问实例变量和调用当前对象的其他方法。
- "super"关键字用于在子类中引用父类的成员,用于调用父类的构造函数、访问父类的实例变量或调用父类的方法。
14.static应用场景
- 静态变量:
- 静态变量是类级别的变量,与类的所有实例共享相同的值。
- 它们可以在类的任何方法中使用,无需创建类的实例。
- 适用于表示全局共享的数据,如计数器、常量或配置信息。
- 静态方法:
- 静态方法是类级别的方法,可以直接通过类名调用,无需创建类的实例。
- 它们不能访问实例变量,只能访问静态变量。
- 适用于不依赖于实例状态,完成独立任务的方法,如工具类中的实用方法、数学计算方法等。
- 静态代码块:
- 静态代码块是在类加载时执行的代码块,在类的第一次使用时执行一次。
- 适用于在类加载过程中进行初始化操作,如初始化静态变量或加载静态资源。
- 静态内部类:
- 静态内部类是定义在类内部的嵌套类,与外部类的实例无关。
- 它们可以直接通过外部类名访问,无需创建外部类的实例。
- 适用于只与外部类相关但不依赖于外部类实例的嵌套类。
- 静态导入:
- 静态导入允许在代码中直接引用其他类的静态成员,而无需使用类名限定符。
- 适用于简化代码书写,提高可读性,但需要谨慎使用以避免命名冲突。
15.static存在的意义?
"static"存在的主要意义是为了实现类级别的成员,而不依赖于类的实例。它提供了以下几个方面的好处:
- 共享数据:
- 简化访问:
- 类级别操作:
- 资源管理
- 嵌套类的独立性
17.什么是面向对象?
数据和相关的操作被封装在对象中,对象之间通过相互交互来完成任务和实现功能。
面向对象编程的核心概念包括以下几个方面:
- 类(Class):类是对象的模板或蓝图,用于定义对象的属性和行为。它是创建对象的基础,描述了对象的特征和行为。
- 对象(Object):对象是类的实例,具有特定的状态(属性)和行为(方法)。通过创建类的实例,可以操作和访问对象的属性和方法。
- 封装(Encapsulation):封装是将相关数据和方法组合到一个单独的实体(类或对象)中的过程。通过封装,对象的内部细节对外部是隐藏的,只有公共接口可用于与对象进行交互。
- 继承(Inheritance):继承是通过定义新的类来扩展已有类的属性和方法的能力。子类可以继承父类的属性和方法,并可以添加新的特性或重写继承的方法。
- 多态(Polymorphism):多态性允许使用统一的接口来处理不同类型的对象。通过多态性,可以使用父类或接口类型的引用来引用子类的对象,从而实现灵活的编程和代码重用。
18.面向对象的特征
-
封装(Encapsulation):
- 封装是将数据和相关操作(方法)封装在一个单独的实体(类或对象)中的过程。
- 封装通过将数据隐藏在对象内部,只提供公共接口来访问和操作数据,从而实现了数据的安全性和保护性。
- 对外部的用户来说,对象的内部细节是不可见的,只能通过公共接口来与对象进行交互。
-
继承(Inheritance):
- 继承是通过定义新的类来扩展已有类的属性和方法的能力。
- 通过继承,子类可以继承父类的属性和方法,并可以添加新的特性或重写继承的方法。
- 继承可以实现代码的重用,减少了代码的冗余,提高了代码的可维护性和可扩展性。
-
多态(Polymorphism):
- 多态性允许使用统一的接口来处理不同类型的对象。
- 多态性基于继承的概念,通过父类或接口类型的引用来引用子类的对象,实现了对象的替代和扩展。
- 多态性使得程序可以根据对象的具体类型选择适当的方法,增加了代码的灵活性和可扩展性。
-
抽象:
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
19.什么是多态机制? Java语言是如何实现多态的?
它允许使用相同的接口来处理不同类型的对象。多态性使得程序能够根据对象的实际类型选择适当的方法,而不必关心对象的具体类型。
在Java语言中,多态性是通过以下两个机制实现的:
- 继承(Inheritance):
- 继承是Java中实现多态性的基础。
- 子类可以继承父类的属性和方法,并可以添加新的特性或重写继承的方法。
- 当父类引用指向子类对象时,可以通过父类类型的引用调用子类重写的方法,实现多态性。
- 方法重写(Method Overriding):
- 方法重写是指子类对父类中的方法进行重新定义,以实现自己的特定行为。
- 当子类重写父类的方法时,可以使用相同的方法签名(名称和参数列表)并提供不同的实现。
- 在运行时,根据对象的实际类型确定要调用的方法,实现多态性。
20.多态的实现
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型(而不是引用变量的类型)决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
21.面向对象五大基本原则?
面向对象编程中有五个基本的设计原则,通常被称为SOLID原则。这些原则有助于设计和组织可维护、灵活和可扩展的软件系统。
- 单一职责原则(Single Responsibility Principle,SRP):
- 即每个类应该只负责一项具体的职责或功能。
- 这样可以提高类的内聚性,使得类更加可理解、可维护和可复用。
- 开放封闭原则(Open-Closed Principle,OCP):
- 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 即软件实体应该通过扩展来实现新的功能,而不是通过修改已有的代码。
- 这样可以保持现有代码的稳定性,并且使得系统更加可扩展和灵活。
- 里氏替换原则(Liskov Substitution Principle,LSP):
- 子类型必须能够替代其基类型(父类)。
- 即在使用基类型的地方,可以使用其子类型而不会产生错误或异常。
- 这样可以确保继承关系的正确性,并使得代码更具扩展性和可维护性。
- 接口隔离原则(Interface Segregation Principle,ISP):
- 不应该强迫客户端依赖它们不使用的接口。
- 即接口应该根据客户端的需要进行细分,使得客户端只需关注所需的接口。
- 这样可以降低类与类之间的耦合度,并提高代码的可复用性和灵活性。
- 依赖倒置原则(Dependency Inversion Principle,DIP):
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
- 即抽象应该依赖于细节,而细节应该依赖于抽象。
- 这样可以实现模块间的松耦合,提高代码的可测试性和可维护性。
22.抽象类和接口的对比?
抽象类和接口是面向对象编程中两种常见的抽象概念,它们具有不同的特点和用途:
抽象类(Abstract Class):
- 抽象类是用于表示一类具有共同特征的对象的抽象概念。
- 抽象类可以包含抽象方法(没有具体实现)和非抽象方法(有具体实现)。
- 抽象类不能被实例化,只能作为父类被继承。
- 子类继承抽象类时,必须实现父类中的所有抽象方法,除非子类本身也是抽象类。
- 抽象类可以拥有属性、构造函数和具体方法的实现。
- 抽象类用于在类的继承层次结构中,提供一个通用的基类,定义共享的行为和属性。
接口(Interface):
- 接口是一种契约或合同,定义了类应该实现的方法集合,而不关心具体的实现细节。
- 接口中只能包含抽象方法和常量(静态final属性)。
- 类通过实现接口来声明自己遵循了该接口的约定。
- 类可以实现多个接口,从而实现多继承的效果。
- 实现接口的类必须实现接口中的所有方法。
- 接口不能直接拥有属性或具体方法的实现,只能定义方法的签名。
共同点 :
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以⽤ default 关键字在接口中定义默认方法)。
区别 :
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- ⼀个类只能继承⼀个类,但是可以实现多个接口。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
23.普通类和抽象类有哪些区别?
普通类和抽象类在面向对象编程中有以下区别:
- 实例化:
- 普通类:可以直接实例化为对象,通过关键字"new"创建类的实例。
- 抽象类:不能直接实例化为对象,只能作为其他类的基类,通过继承来使用。
- 完整性:
- 普通类:具有完整的实现,可以包含属性、方法和构造函数。
- 抽象类:可以包含抽象方法和非抽象方法,可以包含属性和构造函数。
- 继承:
- 普通类:可以被其他类继承,并可以继承其他类。
- 抽象类:可以被其他类继承,并可以继承其他类。抽象类通常用于作为其他类的基类,提供共享的行为和属性。
- 实现:
- 普通类:不需要实现抽象方法,可以直接使用或重写父类的方法。
- 抽象类:可以包含抽象方法,子类必须实现(重写)抽象方法才能实例化。
- 设计目的:
- 普通类:用于描述具体的对象,实现具体的功能和行为。
- 抽象类:用于表示一种通用的概念,提供一些默认的实现,并要求子类提供具体的实现。
- 使用场景:
- 普通类:适用于需要直接实例化为对象,并提供具体实现的场景。
- 抽象类:适用于需要作为其他类的基类,并提供一些默认实现和行为的场景。
24.抽象类能使用final修饰吗?
在Java中,抽象类可以使用final
修饰符进行修饰,但是这两者的修饰含义相互冲突。类的目的是被继承和扩展,而不是被直接实例化。而final
关键字的作用是禁止类的继承和方法的重写。将final
修饰符用于抽象类是没有意义的,
31.什么是内部类?
内部类(Inner Class)是指在一个类的内部定义的另一个类。它是一种嵌套在其他类中的类,可以访问外部类的成员,包括私有成员。内部类的定义可以出现在类的任何位置,包括方法内部、构造函数内部或类的顶层。
内部类有以下几种类型:
- 成员内部类(Member Inner Class):成员内部类是定义在另一个类的内部的普通类。它与外部类之间存在一种包含关系,内部类可以访问外部类的成员,包括私有成员。成员内部类可以使用外部类的实例化对象来创建对象实例。
- 静态内部类(Static Inner Class):静态内部类是定义在另一个类的内部的静态类。它与外部类之间没有包含关系,静态内部类不能直接访问外部类的非静态成员,但可以访问外部类的静态成员。静态内部类的创建不依赖于外部类的对象实例,可以直接使用类名来创建对象实例。
- 方法内部类(Method Local Inner Class):方法内部类是定义在方法内部的类。它的作用域仅限于所在的方法内部,不能在方法外部访问。方法内部类可以访问外部类的成员,并且可以访问方法中的局部变量,但要求局部变量是 final 或 effectively final 的。
- 匿名内部类(Anonymous Inner Class):匿名内部类是一种没有显式名称的内部类。它通常用作临时的、单次使用的类,没有自己的构造方法。匿名内部类常见于在接口或抽象类的实例化对象上定义方法的实现,或者在事件处理程序中定义回调方法的实现。
32.构造器是否可以被重写
它用于初始化对象的状态,不能被继承和重写。
但子类可以通过构造器链来调用父类的构造器,并在自己的构造器中添加额外的初始化逻辑。这样可以确保子类对象在创建时能够同时完成父类和子类的初始化。
34.==和equals的区别是什么?
- 使用 “= =” 比较两个对象时,它会检查它们是否引用同一个内存位置,而不是检查它们的内容是否相等。对于基本类型,如整数或布尔值,"= =" 比较它们的值是否相等。
- “equals” 方法来提供自定义的相等性比较逻辑。 “equals” 是一个方法,可以在对象上调用,以检查两个对象的内容是否相等。默认情况下,“equals” 方法在大多数类中执行与 “==” 相同的引用比较。但是,可以通过在类中重写 “equals” 方法来实现自定义的相等性比较。例如,对于字符串对象,“equals” 方法比较字符串的内容是否相同,而不是比较它们的引用。
综上所述,对于 == 来说,不管什么类型的变量,其本质比的都是值,只是引用类型变量存的值是对象的地址,而 “equals” 是一个方法,用于比较对象的内容是否相等。
35.为什么要有hashCode?和equals关系?
下⾯这段内容摘⾃我的 Java 启蒙书《Head First Java》:
当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashCode 值来判断对象加⼊的位置,同时也会与其他已经加⼊的对象的 hashCode 值作比较,如果没有相符的hashCode , HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调⽤ equals() ⽅法来检查hashCode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让其加⼊操作成功。如果不同的话,就会重新散列到其他位置。这样我们就⼤⼤减少了 equals 的次数,相应就⼤⼤提⾼了执⾏速度。
- hashCode() 的作用是获取哈希码( int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置
- 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
- hashCode() 和 equals() 都是用于比较两个对象是否相等。
-
equals方法:
- equals 方法是在 Object 类中定义的,可以在任何 Java 对象上调用。
- 它用于比较两个对象的内容是否相等。默认情况下,equals 方法执行的是引用比较,即比较两个对象是否引用同一个内存位置。
- 通常情况下,我们需要在自定义类中重写 equals 方法,以便根据对象的属性来进行相等性比较。重写 equals 方法时,我们需要遵循以下约定:
- 对称性:如果 a.equals(b) 返回 true,则 b.equals(a) 也应该返回 true。
- 自反性:一个对象始终应该与自身相等,即 a.equals(a) 应该返回 true。
- 传递性:如果 a.equals(b) 返回 true,并且 b.equals© 也返回 true,则 a.equals© 也应该返回 true。
- 一致性:在对象没有发生变化的情况下,多次调用 equals 方法应该始终返回相同的结果。
- 非空性:对于任何非空引用 a,a.equals(null) 应该返回 false。
-
hashCode方法:
- hashCode 方法也是在 Object 类中定义的,可以在任何 Java 对象上调用。
- 它返回一个整数值,用于表示对象的哈希码。哈希码用于在散列数据结构中进行快速查找。
- 在重写 equals 方法时,通常也需要重写 hashCode 方法。重写 hashCode 方法时,我们需要遵循以下约定:
- 如果两个对象根据 equals 方法比较相等,那么它们的 hashCode 值必须相等。
- 如果两个对象根据 equals 方法比较不相等,它们的 hashCode 值不要求必须不相等,但是不相等的对象应该尽可能产生不同的 hashCode 值,以提高散列数据结构的性能。
- 在 hashCode 方法中使用的字段应该与 equals 方法中使用的相同,以确保一致性。
为什么判断两个对象相等同时提供这两个方法呢?
在⼀些容器(⽐如 HashMap 、 HashSet )中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进 HashSet 的过程)!
添加元素进 HashSet 的过程,如果 HashSet 在对比的时候,同样的 hashCode有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本
这也是先重写hashCode() 再重写equals() 的原因
37.什么是可变长参数?
- 允许在调用方法时传入不定长度的参数。
- 可变参数只能作为函数的最后⼀个参数,但其前面可以有也可以没有任何其他参数。
- 遇到方法重载的情况,优先匹配固定参数的方法。
40.深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
浅拷贝: 浅拷贝会在堆上创建⼀个新的对象(区别于引用拷贝的⼀点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同⼀个内部对象。
深拷贝 : 深拷贝会完全复制整个对象,包括这个对象所包含的内部对象
40.向上转型与向下转型?
-
首先,我们要知道:转型发生在继承后,也就是父类子类存在的前提下。
-
其次,我们要清楚:向下转型的前提是已经发生了向上转型,向下转型是再次转回来而已。
-
向上转型和向下转型都有三种方法:
-
直接赋值
-
作为参数
-
作为返回值类型。
-
-
另外:向下转型要用到强制类型转换,从而完成接收。