面试_Java基础
Java基础
面向对象和面向过程的区别
- 面向过程: 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象:面向对象会将问题先抽象出对象,然后用对象执行方法的方式解决问题。面向对象易维护、易复用、易扩展。因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
但是现在Java的性能也不是很差了。
Java 语言有哪些特点?
简单易学(语法简单,上手容易);
面向对象(封装,继承,多态);
平台无关性( Java 虚拟机实现平台无关性);
支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
可靠性(具备异常处理和自动内存管理机制);
安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源);
高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的);
支持网络编程并且很方便;
编译与解释并存;
Java SE vs Java EE
- Java SE(Java Platform,Standard Edition): Java 平台标准版,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建桌面应用程序或简单的服务器应用程序。
- Java EE(Java Platform, Enterprise Edition ):Java 平台企业版,建立在 Java SE 的基础上,包含了支持企业级应用程序开发和部署的标准和规范(比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序。
简单来说,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。
除了 Java SE 和 Java EE,还有一个 Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。
JVM vs JDK vs JRE
JVM
Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果。(这句话主要说明JVM可以兼容多个平台)
VM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。 也就是说我们平时接触到的 HotSpot VM 仅仅是是 JVM 规范的一种实现而已。
什么是字节码?采⽤字节码的好处是什么
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。(边解释边执行)
Java源程序从源代码到运行的过程如下图所示
我们需要格外注意的是 .class->机器码
这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。(不需要解释了)而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言
JDK 和JRE
JDK(Java Development Kit),它是功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 程序。它拥有JRE所拥有的一切,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等。
JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。不能用于创建新程序
也就是说,JRE 是 Java 运行时环境,仅包含 Java 应用程序的运行时环境和必要的类库。而 JDK 则包含了 JRE,同时还包括了 javac、javadoc、jdb、jconsole、javap 等工具,可以用于 Java 应用程序的开发和调试。如果需要进行 Java 编程工作,比如编写和编译 Java 程序、使用 Java API 文档等,就需要安装 JDK。而对于某些需要使用 Java 特性的应用程序,如 JSP 转换为 Java Servlet、使用反射等,也需要 JDK 来编译和运行 Java 代码。因此,即使不打算进行 Java 应用程序的开发工作,也有可能需要安装 JDK。
Java 和 C++ 的区别?(五点)
虽然,Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:
都是面向对象语言,都支持封装、继承和多态。(共同点)
Java 不提供指针来直接访问内存,程序内存更加安全
Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符,唯一的操作符重载就是String的+号拼接)。
为什么说 Java 语言“编译与解释并存”?
其实这个问题我们讲字节码的时候已经提到过,因为比较重要,所以我们这里再提一下。
我们可以将高级编程语言按照程序的执行方式分为两种:
- 编译型:编译型语言open in new window 会通过编译器open in new window将源代码**一次性翻译**成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
- 解释型:解释型语言open in new window会通过解释器open in new window一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
为了改善解释语言的效率而发展出的即时编译open in new window技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码open in new window。到执行期时,再将字节码直译,之后执行。Javaopen in new window与LLVMopen in new window是这种技术的代表产物。
为什么说 Java 语言“编译与解释并存”?
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
AOT 有什么优点?为什么不全部使用 AOT 呢?
JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation) 。和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码(一次性),属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。并且,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。
可以看出,AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力,可以降低请求的最大延迟。
既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?
我们前面也对比过 JIT 与 AOT,两者各有优点,只能说 AOT 更适合当下的云原生场景,对微服务架构的支持也比较友好。除此之外,AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。然而,很多框架和库(如 Spring、CGLIB)都用到了这些特性。如果只使用 AOT 编译,那就没办法使用这些框架和库了,或者说需要针对性地去做适配和优化。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 .class
文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器
移位运算符
Java 中有三种移位运算符:
Java 移位运算符总结
<<
:左移运算符,向左移若干位,高位丢弃,低位补零。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),左移/右移 42 位相当于左移/右移 10 位(42%32=10)。当 long 类型进行左移/右移操作时,由于 long 对应的二进制是 64 位,因此求余操作的基数也变成了 64。
也就是说:x<<42
等同于x<<10
,x>>42
等同于x>>10
,x >>>42
等同于x >>> 10
。
左移运算符代码示例:
1 | int i = -1; |
输出:
1 | 初始数据:-1 |
基本数据类型
Java 中的几种基本数据类型了解么?
Java 中有 8 种基本数据类型,分别为:
- 6 种数字类型:
- 4 种整数型:
byte
、short
、int
、long
- 2 种浮点型:
float
、double
- 4 种整数型:
- 1 种字符类型:
char
- 1 种布尔型:
boolean
。
这 8 种基本数据类型的默认值以及所占空间的大小如下:
基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
---|---|---|---|---|
byte |
8 | 1 | 0 | -128 ~ 127 |
short |
16 | 2 | 0 | -32768(-2^15) ~ 32767(2^15 - 1) |
int |
32 | 4 | 0 | -2147483648 ~ 2147483647 |
long |
64 | 8 | 0L | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) |
char |
16 | 2 | ‘u0000’ | 0 ~ 65535(2^16 - 1) |
float |
32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double |
64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
boolean |
1 | false | true、false |
- Java 里使用
long
类型的数据一定要在数值后面加上 L,否则将作为整型解析。 char a = 'h'
char :单引号,String a = "hello"
:双引号。
这八种基本类型都有对应的包装类分别为:Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
基本数据类型和包装类型的区别?(五点)
1.用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
2.存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
3.占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
4.默认值:成员变量包装类型不赋值就是 null
,而基本类型有默认值且不是 null
。
5.比较方式:对于基本数据类型来说,==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals()
方法。
⚠️ 注意:基本数据类型存放在栈中是一个常见的误区! 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中。
1 | public class Test { |
运行时常量池是方法区的一部分,它主要用于存储编译期生成的各种字面量和符号引用。这些字面量包括字符串、数字等,而符号引用则用于描述类和接口的引用。在Java程序中,当一个字符串常量被定义时,它会被添加到常量池中。
另一方面,类变量(静态变量)是存放在Java虚拟机(JVM)的方法区中的。方法区是JVM中的一块内存区域,用于存储类的信息、静态变量、常量池等数据。
包装类型的缓存机制了解么?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
Integer 缓存源码:
1 | public static Integer valueOf(int i) { |
Character
缓存源码:
1 | public static Character valueOf(char c) { |
Boolean
缓存源码:
1 | public static Boolean valueOf(boolean b) { |
1 | //同一个对象,只对自动装箱机制有效 |
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float
,Double
并没有实现缓存机制。
1 | Integer i1 = 33; |
下面我们来看一个问题:下面的代码的输出结果是 true
还是 false
呢?
1 | Integer i1 = 40; |
Integer i1=40
这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40)
。因此,i1
直接使用的是缓存中的对象。而Integer i2 = new Integer(40)
会直接创建新的对象。
因此,答案是 false
。你答对了吗?
记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()
方法(例如:Integer.valueof
),拆箱其实就是调用了 xxxValue()
方法。
都是调用了包装类的方法
因此,
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
包装类在执行加减乘除的时候就会执行拆箱操作
1 | private static long sum() { |
局部变量和成员变量的区别(五点)
归属:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;
修饰符:成员变量可以被 public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及 static
所修饰;但是,成员变量和局部变量都能被 final
所修饰。
存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static
修饰的,那么这个成员变量是属于类的,会存储在方法区。如果没有使用 static
修饰,这个成员变量是属于实例的,存在于堆内存。局部变量则存在于栈的局部变量表上。
生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值(必须手动赋值)。
静态变量有什么作用?
静态变量也就是被 static
关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,即使创建多个对象,静态变量只会被分配一次内存,这样可以节省内存。
通常情况下,静态变量会被 final
关键字修饰成为常量。
1 | public class ConstantVariableExample { |
字符型常量和字符串常量的区别?(三点)
形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。
静态方法和实例方法有何不同?
1、调用方式
在外部调用静态方法时,可以使用 类名.方法名
的方式,也可以使用 对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
不过,需要注意的是一般不建议使用 对象.方法名
的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。
因此,一般建议使用 类名.方法名
的方式来调用静态方法。
1 | public class Person { |
2、访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制,都可以访问
重载和重写有什么区别?
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,你可以做出有别于父类的响应
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
什么是可变长参数?
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 printVariable
方法就可以接受 0 个或者多个参数。
1 | public static void method1(String... args) { |
另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
1 | public static void method2(String arg1, String... args) { |
遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?
答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
我们通过下面这个例子来证明一下
1 | /** |
输出:
1 | ab |
另外,Java 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的 class
文件就可以看出来了。
1 | public class VariableLengthArgument { |
如果一个类没有声明构造方法,该程序能正确执行吗?
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。
我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩。
构造方法有哪些特点?是否可被 override?(四点)
构造方法特点如下:
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 新建对象时自动执行,无需调用。
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
在 Java 中定义⼀个不做事且没有参数的构造方法的作⽤
Java 程序在执⾏⼦类的构造⽅法之前,如果没有⽤ super()
来调⽤⽗类特定的构造⽅法,则会调⽤⽗类中“没有参数的构造⽅法”。因此,如果⽗类中只定义了有参数的构造⽅法,⽽在⼦类的构造⽅法中⼜没有⽤ super()
来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为 Java 程序在⽗类中找不到没有参数的构造⽅法可供执⾏。解决办法是在⽗类⾥加上⼀个不做事且没有参数的构造⽅法。
super
相当于给父类加上一个无参构造方法
“一句话概括就是帮助子类做初始化“
面向对象编程三大特性
封装
封装把⼀个对象的属性私有化,同时提供⼀些可以被外界访问的属性的⽅法,如果属性不想被外界访问,我们⼤可不必提供⽅法给外界访问。但是如果⼀个类没有提供给外界访问的⽅法,那么这个类也没有什么意义了。
继承
继承是使⽤已存在的类的定义作为基础建⽴新类的技术,子类可以继承父类的所有非私有(public、protected)属性和方法,并且可以添加新的属性或重写父类的方法。
关于继承如下 3 点请记住:
⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法,⼦类是⽆法访问,只是拥有。
⼦类可以拥有⾃⼰属性和⽅法,即⼦类可以对⽗类进⾏扩展。
⼦类可以重写父类的方法
多态
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
所谓多态就是指程序中的引⽤变量所指向的具体类型和通过该引⽤变量发出的方法调⽤**在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。在 Java 中有两种形式可以实现多态:继承(多个⼦类对同⼀⽅法的重写)和接⼝(实现接⼝并覆盖接⼝中同⼀⽅法)。
接口和抽象类的共同点和区别(五点)
共同点
二者都不能被实例化
二者都可以包含抽象方法
区别
成员种类:接口中只包含常量(在Java中,接口中的字段默认都是
public static fina
l的)、抽象方法和默认方法(从Java 8开始)。而抽象类除了包含抽象方法外,还可以包含普通方法、属性、静态方法以及初始化块等。继承与实现:一个类可以实现多个接口,但只能继承一个抽象类(Java中,使用
extends
关键字继承类,使用implements
关键字实现接口)。访问修饰符:接口中的每一个方法都是
public
修饰的抽象方法,即接口中的方法会被隐式指定为public abstract
。而抽象类中的方法可以有不同的访问修饰符。从设计层面上来说:抽象类是对类的抽象,是一种模版设计,而接口是对行为的抽象,是一种行为规范
1 | public interface MyInterface { |
在这个例子中,MyInterface
接口有一个普通的方法声明method1()
和一个默认方法method2()
。任何实现MyInterface
的类都必须提供method1()
的实现,但可以选择性地覆盖method2()
的默认实现。
深拷贝和浅拷贝
**浅拷贝:**会创建一个新对象,并进行属性复制。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。一般使用Object的Clone()方法来实现。
深拷贝:深拷贝则是会新建一个对象,然后完整的复制整个对象,包括引用类型的的属性。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
浅拷贝
浅拷贝的示例代码如下,我们这里实现了 Cloneable
接口,并重写了 clone()
方法。
clone()
方法的实现很简单,直接调用的是父类 Object
的 clone()
方法。
1 | public class Address implements Cloneable{ |
深拷贝
这里我们简单对 Person
类的 clone()
方法进行修改,连带着要把 Person
对象内部的 Address
对象一起复制。
1 |
|
== 和 equals() 的区别
==
对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址。
equals()
方法存在两种使用情况:
- 类没有重写
equals()
方法:通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。 - 类重写了
equals()
方法:一般我们都重写equals()
方法来比较两个对象中的内容是否相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。
注意:String
中的 equals
方法是被重写过的,因为 Object
的 equals
方法是比较的对象的内存地址,而 String
的 equals
方法比较的是对象的值。
当创建 String
类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String
对象。
HashCode
介绍
hashCode()
的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个int 整数。
这个哈希码的作⽤是确定该对象在哈希表中的索引位置。
hashCode()
定义在 JDK 的 Objec
t 类中,这就意味着 Java 中的任何类都包含有 hashCode()
函数。另外需要注意的是: Object
的 hashcode
⽅法是本地⽅法,也就是⽤ c 语⾔或 c++ 实现的,该⽅法通常⽤来将对象的 内存地址 转换为整数之后返回。
1 | public native int hashCode(); |
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode?
我们以“HashSet
如何检查重复”为例子来说明为什么要有 hashCode
?
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
当你把对象加入
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较,如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。但是如果发现有相同hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
其实, hashCode()
和 equals()
都是用于比较两个对象是否相等。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
总结:
equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。- 两个对象有相同的
hashCode
值,他们也不一定是相等的(哈希碰撞)。
那为什么两个对象有相同的 hashCode
值,它们也不一定是相等的?
因为 hashCode()
所使用的哈希算法(杂凑算法)也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode
)。
总结下来就是:
- 如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。 - 如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等
String类型
String、StringBuffer、StringBuilder 的区别?(三点+1总结)
1.可变性
String
是不可变的,使用final
关键字来保存,private final char value[]
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用 final
和 private
关键字修饰,所以这两个对象是可变的
1 | abstract class AbstractStringBuilder implements Appendable, CharSequence { |
2.线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
3.性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
字符串拼接用“+” 还是 StringBuilder?
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
1 | String str1 = "he"; |
可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显:编译器不会创建单个 StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。
StringBuilder
对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder
对象。
如果直接使用 StringBuilder
对象进行字符串拼接的话,就不会存在这个问题了。
1 | String[] arr = {"he", "llo", "world"}; |
字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
1 | // 在堆中创建字符串对象”ab“ |
String s1 = new String(“abc”);这句话创建了几个字符串对象?
- 会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在**堆上创建两个字符串对象**,其中一个字符串对象的引用会被保存在字符串常量池中。
示例代码(JDK 1.8):
1 | String s1 = new String("abc"); |
对应的字节码:
ldc
命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。
2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
示例代码(JDK 1.8):
1 | // 字符串常量池中已存在字符串对象“abc”的引用 |
String#intern 方法有什么作用?
String.intern()
是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
1 | // 在堆中创建字符串对象”Java“ |
异常
Java 异常类层次结构图概览:
Java 异常类层次结构图
Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。- **
Error
**:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译。
比如下面这段 IO 操作的代码:
除了RuntimeException
及其子类以外,其他的Exception
类及其子类都属于受检查异常(Checked Exception) 。常见的受检查异常有:IO 相关的异常、ClassNotFoundException
、SQLException
…。**
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException
及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)- ……
Throwable 类常用方法有哪些?
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
1 | try |
1 | message = / by zero |
try-catch-finally 如何使用?
try
块:用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。catch
块:用于处理 try 捕获到的异常。finally
块:无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行,**并且返回的值会覆盖catch中返回的值**
finally 中的代码一定会执行吗?
不一定的!在某些情况下,finally 中的代码不会被执行。
就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
trow 和trows
throw
用于手动抛出异常,通常与try-catch
语句配合使用。throws
用于声明方法可能抛出的异常,让调用者知道需要处理这些异常。throw
语句放在方法体内部,throws
关键字放在方法声明的后面。
泛型
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
泛型是Java等编程语言中的一种特性,它允许程序员在定义类、接口和方法时使用类型参数(即占位符类型),以便在实例化或调用时传入具体的类型,实现类型的安全性和可重用性
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
什么是泛型擦除机制?为什么要擦除
Java的泛型其实是一种伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦除,也就是说在编译后的字节码中会去除所有泛型类型信息,这也就是通常所说类型擦除,举个例子例List<String>
在运行时会被视为List
,并且其中的元素被视为Object
类型。
实际上,就算一个集合指定了Integer的泛型,实际上你还是可以往里面添加String类型的,因为泛型的实际类型是object的方法,但是要使用反射方法。
为什么要进行泛型擦除
- 兼容老版本的Java编译器和虚拟机:在JDK 1.5之前,Java是没有泛型概念的。为了兼容这些老版本的编译器和虚拟机,Java引入了泛型擦除机制。当编译带有泛型的Java代码时,编译器会将泛型类型信息擦除,替换为原生类型或类型边界(如果存在的话),以确保生成的字节码可以在没有泛型支持的JVM上运行。
- 避免代码膨胀和内存占用:如果为每个泛型类型都生成不同的目标代码,那么对于大量使用泛型的代码库来说,将会导致代码膨胀和内存占用增加。通过泛型擦除,可以重用相同的字节码,减少内存占用。
- 简化JVM设计:由于JVM在运行时不需要处理泛型类型信息,因此可以简化JVM的设计和实现。
为什么要使用泛型
使用泛型可在编译期间进行类型检查,以及提高类型安全
使用Object类型需要手动添加强制类型转换,降低代码可读性,提高出错概率。
泛型可以使用自限定类型如T extends Comparable。
通配符
泛型类型是固定的,某些场景下使用起来不太灵活,于是,通配符就来了!通配符可以允许类型参数变化,用来解决泛型无法协变的问题。
通配符(Wildcard)
通配符主要用于泛型编程中,特别是在处理泛型集合时。它提供了在不知道或不关心集合中具体类型参数的情况下处理集合的能力。通配符主要有两种:?
(无界通配符)和? extends T
(有界通配符)。
- 无界通配符(Unbounded Wildcard):
?
表示未知的类型。当你只知道集合包含某种类型的对象,但不知道具体是什么类型时,可以使用无界通配符。
示例:
无界通配符可以接收任何泛型类型数据,用于实现不依赖于具体类型参数的简单方法,可以捕获参数类型并交由泛型方法进行处理。
1 | void testMethod(Person<?> p) |
1 | List<?> list = new ArrayList<String>(); // list的泛型类型为未知类型 |
- 有界通配符(Bounded Wildcard):
? extends T
表示未知的类型,但它是T或T的子类。这允许你在编译时提供一些类型信息,同时仍然保持灵活性。
示例:
1 | List<? extends Number> numberList = new ArrayList<Integer>(); // numberList的泛型类型为Number或其子类 |
区别总结
- 泛型:在定义类、接口和方法时使用类型参数,提供了编译时的类型检查和类型安全。
- 通配符:主要用于泛型编程中,特别是处理泛型集合时,允许在不知道或不关心具体类型参数的情况下处理集合。
注意:虽然通配符和泛型在某些情况下可以互换使用,但它们各自有特定的用途和限制。在编写泛型代码时,应仔细考虑何时使用泛型、何时使用通配符,以及使用哪种类型的通配符。
使用了通配符的话,是不是就不能添加元素了(了解即可)
不完全是
- 读取操作:当您有一个带有通配符的集合引用时(例如,
List<?>
),您仍然可以从该集合中读取元素。这是因为您不需要知道确切的元素类型就可以读取它们(它们会被视为Object
类型)。 - 添加操作:当您尝试向带有通配符的集合中添加元素时,会遇到问题。例如,如果您有一个
List<?> list
,并且尝试执行list.add(someObject)
,编译器会报错,因为它不知道someObject
的确切类型是否与集合中现有元素的类型兼容。但是,有一种情况例外,那就是使用通配符的上界(? extends T
),但即使在这种情况下,您也不能直接添加元素,因为您仍然不知道确切的类型。 - 带有上界(
? extends T
)的通配符:这种通配符允许您读取集合中的元素,但不允许您添加元素(除了null
,因为null
是所有类型的成员)。这是因为您不知道集合中实际存储的元素的类型,所以添加任何具体的T
类型或其子类都可能破坏集合的类型安全性。 - 带有下界(
? super T
)的通配符:这种通配符允许您向集合中添加类型为T
或其子类的元素,因为您知道集合能够容纳这些类型的元素。但是,使用这种通配符时,您不能从集合中读取具有具体类型T
的元素,因为返回的对象将是Object
类型,除非您进行显式的类型转换。
通配符和常泛型T之间有什么区别?
T可以用于声明变量或常量而?不行。
T一般用于声明泛型类或方法,通配符?一般用于泛型方法的调用代码和形参。
T在编译期会被擦除为限定类型或Object,通配符用于捕获具体类型。
反射
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
通过反射,我们可以在运行时获取类的Class对象,然后通过这个Class对象来获取类的所有成员信息(包括构造方法、方法、字段等),并且可以在运行时创建对象、调用方法或访问和修改字段的值。
谈谈反射机制的优缺点
优点:可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
缺点:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
获取 Class 对象的四种方式
如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class 对象:
1. 知道具体类的情况下可以使用:
1 | Class alunbarClass = TargetObject.class; |
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
2. 通过 Class.forName()
传入类的全路径获取:
1 | Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); |
3. 通过对象实例instance.getClass()
获取:
1 | TargetObject o = new TargetObject(); |
4. 通过类加载器xxxClassLoader.loadClass()
传入类路径获取:
1 | ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject"); |
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行
序列化
简单来说:
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
下面是序列化和反序列化常见应用场景:
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
序列化协议对应于 TCP/IP 4 层模型的哪一层?
我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢?
- 应用层
- 传输层
- 网络层
- 网络接口层
TCP/IP 四层模型
如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?
因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,可以使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
关于 transient
还有几点注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
IO
Java中IO流分为几种
按照流的流向分,可以分为输⼊流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的⻆⾊划分为节点流和处理流。
Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,⽽且彼此之间存在⾮常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派⽣出来的。
InputStream/Reader:
所有的输⼊流的基类,前者是字节输⼊流,后者是字符输⼊流。
OutputStream/Writer:
所有输出流的基类,前者是字节输出流,后者是字符输出流。
既然有了字节流为什么还要有字符流
操作字符需要 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是⾮常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流比较好,如果涉及到字符的话使⽤字符流比较好。
BIO,NIO,AIO有什么区别
BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完成。在活动连接数不是特别⾼(⼩于单机 1000)的情况下,这种模型是比较不错的,可以让每⼀个连接专注于⾃⼰的 I/O 并且编程模型简单,也不⽤过多考虑系统的过载、限流等问题。线程池本身就是⼀个天然的漏⽃,可以缓冲⼀些系统处理不了的连接或请求。但是,当⾯对⼗万甚⾄百万级连接的时候,传统的 BIO 模型是⽆能为⼒的。因此,我们需要⼀种更⾼效的 I/O 处理模型来应对更⾼的并发量。
NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型,在 Java 1.4 中引⼊了NIO 框架,对应 java.nio 包,提供了 Buffer,Channel, Selector 等抽象所以。它⽀持⾯向缓冲的,基于通道的 I/O 操作⽅法。NIO 中的 N 不仅可以理解为 Non-blocking,不阻塞的意思。还可以理解成New Blocking。一种新的阻塞形式。NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,两种通道都⽀持阻塞和⾮阻塞种模式。对于⾼负载、⾼并发的(⽹络)应⽤,应使⽤ NIO 的⾮阻塞模式来开发
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是异步⾮阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应⽤操作之后会直接返回,不会堵塞在那⾥,当后台处理完成,操作系统会通知相应的线程进⾏后续的操作。⽬前来说 AIO 的应⽤还不是很⼴泛,Netty 之前也尝试使⽤过 AIO,不过⼜放弃了
VUE_APP_URL=’http://120.79.200.251:7999/‘
VUE_APP_DOCDOWNURL=’http://120.79.200.251:8199/‘
#VUE_APP_URL=’https://order-book.rohedu.com/api‘
#VUE_APP_DOCDOWNURL=’https://order-book.rohedu.com/downLoad/‘