回复面试,加入Java面试交流群
大家好,我是哪吒。
本系列是【 10万字208道Java经典面试题总结(附答案) 】的2024修订版。
1、说说跨平台性
我们希望编写好的代码和程序,最好可以在任意平台和环境下运行,而不需要根据不同的平台,编写不同的代码。
比如,我编写的一个博客管理系统,我希望它可以在windows中运行,也可以在Linux环境下运行,也可以在MacOS环境下运行。
这就是跨平台特性,节省开发和运维成本。
2、Java是如何实现跨平台性的?
Java实现跨平台性的关键在于JVM虚拟机,Java语言编写的程序会被编译成与平台无关的字节码文件,这些字节码文件可以在任何装有Java虚拟机的系统上运行,因为Java字节码不针对特定的操作系统或硬件,而是设计成一种中间代码,可以在不同平台上被解释执行。
JVM是Java跨平台的核心组件。它作为一个运行时环境,负责加载字节码并将其解释或编译为特定平台的机器代码。每个操作系统都有其专用的JVM实现,例如Windows、Linux和macOS等。
Java源码只需编译一次,将java文件编译成 class文件,就可以通过安装在Windows或Linux中的JVM中运行。
3、JDK 和 JRE 有什么区别?
JDK是Java开发工具包,它包含了JRE和开发工具(如javac编译器和java程序运行工具等),主要用于Java程序的开发。而JRE是Java运行环境,它只包含了运行Java程序所必须的环境,主要用于Java程序的运行。
JDK面向的是Java开发者,提供了Java开发工具,如编译工具(javac)和打包工具(jar)等,方便开发者进行Java程序的开发和调试。而JRE面向的是Java程序的使用者,只要安装了JRE,就可以在对应的操作系统上运行Java程序。
JDK由JRE和Java开发工具组成,包含了Java语言所必须的类库和Java虚拟机(JVM),以及编译器和调试器等开发工具。而JRE只包含Java虚拟机(JVM)和Java语言所必须的类库,这些类库对于开发来说是足够的,但对于开发过程中的编译和调试并不足够。
4、为何要配置Java环境变量?
path环境变量的作用就是告诉系统,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到哪些目录下去寻找。而我们通常情况下配置的path变量,就是这个目录的完整路径。
简而言之,就是运行某个命令需要某些参数时,如果当前目录下找不到,则自动去环境变量中寻找。
5、Java都有哪些特性?
这是一道没有标准回答的面试题,按照下面的内容陈列即可,回答是切记刻板背诵。
(1)面向对象
Java是一个面向对象的语言,万物皆对象。
面向对象是一种程序设计规范,其基本思想是使用对象、类、继承、封装、多态等基本概念来进行程序设计。从现实世界中客观存在的事物(即对象)出发来构造软件系统,并且在系统构造中尽可能运用人类的自然思维方式。
程序就是由无数个对象+对象的行为组装而成,当程序请求某个对象时,实际上就是通过一个请求,将「你自己的对象」传递给程序,程序经过一系列匪夷所思的操作,再将一个新的对象返给你。
从底层角度而言,每一个对象都是由其它对象组成的,只是它们的维度、粒度不同而已。
每一个对象都有不同的属性。
比如芯片+电路板组成了内存,内存+硬盘+cpu+机箱组成了电脑,芯片、电路板、内存、硬盘、CPU、机箱、电脑,都是一个对象,只不过粒度不同罢了。
对象具有属性、方法,在内存中的地址都是唯一的。
(2)分布式
分布式系统对于用户而言,他们面对的就是一个服务器,提供用户需要的服务而已,而实际上这些服务是通过背后的众多服务器组成的一个分布式系统,因此分布式系统看起来像是一个超级计算机一样。
(3)健壮性
为了获得可靠性,Java在一些关键领域进行了限制,从而迫使程序员在程序开发中及早地发现错误。
因为Java是强类型化的语言,它在编译时检查代码。当然不管怎样,在运行时也检查代码。许多难以跟踪的bug,在运行时通 常难以再现,这种情况在Java中几乎不可能产生。因为使编写好的程序在不同的运行条件下,以可预见的方式运行是Java的关键特性之一。
在传统的编程环境中,内存管理是一件困难、乏味的工作。例如,在C/C++中,程序员必须手动分配和释放所有动态内存。Java通过为您管理内存的分配和释放,可以从根本上消除这些问题(事实上,释放内存完全是自动的,因为Java为不再使用的对象提供了垃圾回收功能)。
(4)安全性
Java取消了强大但又危险的指针,而代之以引用。由于指针可进行移动运算,指针可随便指向一个内存区域,而不管这个区域是否可用,这样做是危险的,因为原来这个内存地址可能存储着重要数据或者是其他程序运行所占用的,并且使用指针也容易数组越界。
垃圾回收机制:不需要程序员直接控制内存回收,由垃圾回收器在后台自动回收不再使用的内存。避免程序忘记及时回收,导致内存泄露。避免程序错误回收程序核心类库的内存,导致系统崩溃。
异常处理机制:Java异常机制主要依赖于try、catch、finally、throw、throws五个关键字。
强制类型转换:只有在满足强制转换规则的情况下才能强转成功。
Java在字节码的传输过程中使用了公开密钥加密机制(PKC)。
在运行环境提供了四级安全性保障机制:字节码校验器 -类装载器 -运行时内存布局 -文件访问限制。
(5)体系结构中立
编译器生成一个体系结构中立的目标文件格式,这是一种编译过的代码,只要有Java运行时系统,就可以在许多处理器上运行。Java编译器通过生成与特定的计算机体系结构无关的字节码指令来实现这一特性。精心设计的字节码不仅可以很容易地在任何机器上解释执行,而且还可以迅速地翻译成本地机器的代码。
字节码实现了结构中立,与计算机结构无关。
(6)可移植性
Java语言之中最大的特点在于其可移植性的支持,所谓的可移植性指的是同一个程序可以在不同的操作系统之间任意的进行部署,这样就减少了开发的难度,在Java里面如果要想实现可移植性的控制,那么主要依靠的是JVM(Java 虚拟机)。Java虚拟机是一个由软件和硬件模拟出来的计算机,所有的程序只要有Java虚拟机的支持,那么就可以实现程序的执行,并且不同的操作系统上会有不同版本的JVM存在,这样就可以实现移植性。
(7)解释性
有人说Java是编译型的。因为所有的Java代码都是要编译的,.java不经过编译就无法执行。也有人说Java是解释型的。因为java代码编译后不能直接运行,它是解释运行在JVM上的,所以它是解释型的。
(8)高性能
即时编译器可以监控经常执行哪些代码并优化这些代码以提高速度。更为复杂的优化是消除函数调用(即内联)。即时编译器知道哪些类已经被加载。基于当前加载的类集,如果特定的函数不会被覆盖,就可以使用内联。必要时,还可以撤销优化。
(9)多线程
指的是这个程序(一个进程)运行时产生了不止一个线程。
(10)动态性
Java本质为静态语言,而不是动态语言。动态语言显著的特点是在程序运行时,可以改变程序结构或变量类型,典型的动态语言有Python、ruby、javascript等。Java不是动态语言,但Java具有一定的动态性,表现在以下几个方面:
反射机制;
动态字节码操作;
动态编译;
执行其他脚本代码;
6、== 和 equals 的区别是什么?
(1)类型与定义
==是一个操作符,用于比较两个变量的值。对于基本数据类型,它比较的是变量存储的值是否相等;对于引用类型,它比较的是两个引用是否指向内存中的同一地址(即是否是同一个对象的引用)。equals()是Object类中的一个方法,用于比较两个对象的内容是否相等。默认情况下,它比较的是对象的内存地址(即是否是同一个对象),但该方法可以被重写以提供自定义的比较逻辑。
(2)运行速度
==:由于只是比较引用或内存地址,所以通常比equals()方法更快。
equals():由于可能需要执行更复杂的比较逻辑(尤其是在被重写的情况下),因此其运行速度可能慢于==。
(3)可重写性
==:不可重写,其行为是固定的。equals():可以被重写以提供自定义的相等性判断逻辑。
下面程序的输出结果是什么?
public classMain{
publicstaticvoidmain(String[] args){
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d);//true
System.out.println(e==f);//false
System.out.println(c==(a+b));//true
System.out.println(c.equals(a+b));//true
System.out.println(g==(a+b));//true
System.out.println(g.equals(a+b));//false
System.out.println(g.equals(a+h));//true
}
}
当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。另外,对于包装器类型,equals方法并不会进行类型转换。
第一个和第二个输出结果没有什么疑问。第三句由于 a+b包含了算术运算,因此会触发自动拆箱过程(会调用intValue方法),因此它们比较的是数值是否相等。而对于c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较。同理对于后面的也是这样,不过要注意倒数第二个和最后一个输出的结果(如果数值是int类型的,装箱过程调用的是Integer.valueOf;如果是long类型的,装箱调用的Long.valueOf方法)。
7、Java中有哪些数学函数?
Java中提供了一些数学函数,位于java.lang.Math类中。这些函数包括以下几种:
基本数学函数:abs、max、min。指数函数:exp、log、pow。三角函数:sin、cos、tan、asin、acos、atan。双曲函数:sinh、cosh、tanh、asinh、acosh、atanh。随机数函数:random。
8、Java中有哪些位运算符?
(1)与运算符 &
只有两个位都是1,结果才是1
package com.nezha.javase;
public classTest{
publicstaticvoidmain(String[] args){
int x = 129;
int y = 128;
System.out.println("x 和y 与的结果是:"+(x&y)); // 128
}
}
x 二进制 10000001;
y 二进制 10000000;
根据与运算符的运算规律,只有两个位都是1,结果才是1,可以知道结果就是10000000,即128。
(2)或运算符 |
两个位只要有一个为1,那么结果就是1,否则就为0。
package com.nezha.javase;
public classTest{
publicstaticvoidmain(String[] args){
int x = 129;
int y = 128;
System.out.println("x 和y 或的结果是:"+(x|y)); // 129
}
}
(3)非运算符 ~
如果位为0,结果是1,如果位为1,结果是0。
(4)异或运算符 ^
两个操作数的位中,相同则结果为0,不同则结果为1。
(5)左移运算符>>、右移运算符<<
移位运算符的右操作数要完成模32的运算。
1<<35的值等同于1<<3或8。
publicstaticvoidmain(String[] args){
System.out.println(1 <<35);//8
System.out.println(1<<3);//8
}
(6)>>>运算符会用0填充高位,不存在<<<运算符。
9、说说运算符的优先级
这道题属实有点变态,这谁能记得住,运算符优先级只是一种约定,实际使用时应根据具体情况加上括号以明确运算顺序。
Java中的运算符按照优先级从高到低依次为:
():括号运算符,具有最高优先级;
!、~、++、--:逻辑非、位取反、自增、自减运算符;
*、/、%:乘、除、取模运算符,优先级相同,从左向右结合;
+、-:加、减运算符,优先级相同,从左向右结合;
<<、>>、>>>:左移、右移、无符号右移运算符,优先级相同,从左向右结合;
<、<=、>、>=:小于、小于等于、大于、大于等于运算符,优先级相同,从左向右结合;
==、!=:等于、不等于运算符,优先级相同,从左向右结合;
&:按位与运算符,优先级较低;
^:按位异或运算符,优先级更低;
|:按位或运算符,优先级最低;
&&:逻辑与运算符,优先级较低;
||:逻辑或运算符,优先级更低;
?::三元条件运算符,优先级最低;
10、final 在 java 中有什么作用?
(1)用来修饰一个引用
如果引用为基本数据类型,则该引用为常量,该值无法修改;如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
(2)用来修饰一个方法
当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
(3)用来修饰类
当用final修改类时,该类成为最终类,无法被继承。
比如常用的String类就是最终类。
11、使用 final 关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?
当使用final关键字修饰变量时,这意味着该变量的引用地址被固定,它不能再指向另一个对象或值。然而,这并不意味着该引用所指向的对象的内容也不能改变。事实上,如果这个变量指向的是一个可变对象(如数组、集合或任何其他用户定义的可变类实例),那么对象的内容是可以修改的。
12、this 和super 关键字的作用
(1)this关键字的作用
对象内部指代自身的引用;
解决成员变量和局部变量的同名问题;
可以调用成员变量,不能调用局部变量;
可以调用成员方法。
(2)super关键字的作用
调用父类的成员变量或方法
调用父类的构造函数
13、在 Java 中,为什么不允许从静态方法中访问非静态变量?
静态变量属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问;
非静态变量属于类的对象,只有在类的对象产生时,才会分配内存,通过类的实例去访问;
静态方法也属于类本身,但是此时没有类的实例,内存中没有非静态变量,所以无法调用。
14、final 与 static 的区别?
当一个变量被声明为final时,它的值在初始化后不能被改变。对于引用类型,它指的是引用不可变,即不能再指向其他对象,但对象本身的状态可能改变。当方法被声明为final时,该方法不能被子类重写。当一个类被声明为final时,该类不能被继承。
当一个变量或方法被声明为static时,它属于类而非类的实例。静态变量在内存中只有一份,无论创建多少个类的实例,所有实例共享同一个静态变量。静态方法可以直接通过类名调用,无需创建类的实例。静态代码块在类加载时执行,通常用于系统初始化。
final关键字主要用于声明常量、防止继承和阻止方法重写,而static关键字主要用于实现与类相关联的变量和方法,以及控制类的初始化过程。
15、int可以强制转换为byte吗?
可以进行强制转换,在Java中,int是32位,byte是8位,如果强制转换,int类型的高24位将会被丢弃。
16、char 型变量中能存储一个中文汉字吗?
在 Java 中,char 类型占 2 个字节,而且 Java 默认采用Unicode 编码,一个Unicode 码占16 位,所以一个 Unicode 码占两个字节,char 类型变量可以存储一个中文汉字。
17、byte类型127+1等于多少
byte的范围是-128~127。
字节长度为8位,最左边的是符号位,而127的二进制为01111111,所以执行+1操作时,01111111变为10000000。
大家知道,计算机中存储负数,存的是补码的兴衰。左边第一位为符号位。
那么负数的补码转换成十进制如下:
一个数如果为正,则它的原码、反码、补码相同;一个正数的补码,将其转化为十进制,可以直接转换。
已知一个负数的补码,将其转换为十进制数,步骤如下:
先对各位取反;
将其转换为十进制数;
加上负号,再减去1;
例如10000000,最高位是1,是负数,①对各位取反得01111111,转换为十进制就是127,加上负号得-127,再减去1得-128;
下面这段代码的输出结果是什么?
public classMain{
publicstaticvoidmain(String[] args){
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
也许有些朋友会说都会输出false,或者也有朋友会说都会输出true。但是事实上输出结果是:
true
false
为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:
publicstatic Interger valueOf(int i){
if(i>=-128&&i<=IntergerCache.high){
return IntergerCache.cache[i+128];
}else{
returnnew Interger(i);
}
}
通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。
其它的引用类型,可以去查看valueOf的实现。
18、为什么数组的起始索引是0而不是1?
这个习惯来源于机器语言,那时要计算一个数组元素的地址需要将数组的起始地址加上该元素的索引。将起始索引设为1要么浪费数组的第一个元素的空间,要么会花费额外的时间来将索引减1。
19、什么是机器语言?
机器语言是一种指令集的体系,是最早出现的计算机语言。机器语言从属于硬件设备。不同的计算机设备有不同的机器语言.所以机器语言是一种面向机器的语言。计算机指令系统中的指令是由「0」和「1」两种符号组成的代码,并且能被机器直接理解执行,它们被称为机器指令。一个计算机的机器指令的集,就构成了该计算机的机器语言,即计算机可以直接接受、理解的语言。
机器语言能利用机器指令精准地描述算法、且编程质量高、所占存储空间小,执行速度快。但是这种程序直观性很差,容易出错,阅读检查和修改调试非常困难。
20、什么是汇编语言?
汇编语言是一种低级计算机编程语言,它使用一种非常接近于计算机硬件的指令系统。因此,汇编语言也被认为是一种次级的计算机语言。
汇编语言的特点包括:
汇编语言可以提供对计算机硬件的直接访问,因此它被用于编写操作系统和嵌入式系统等高性能的程序。
汇编语言具有非常高的执行效率,因为它不需要进行高级语言的编译,也不需要进行解释,可以直接在硬件上执行。
汇编语言的代码密度非常高,因为它的指令系统非常紧凑,可以有效地利用内存空间。
汇编语言的执行速度非常快,因为它的指令可以直接被计算机硬件理解并执行。
汇编语言需要程序员有更深入的计算机体系结构和硬件知识,因为它的指令系统比较复杂,编写难度也比较大。
总的来说,汇编语言是一种面向机器的低级语言,它直接访问计算机硬件,具有高执行效率和代码密度等优点。但因为它的指令系统复杂,编写难度较大,需要程序员有较高的技术水平。
21、Java属于什么语言?
Java属于高级语言的一种,高级语言是一种与具体硬件和操作系统无关的编程语言,它更接近于自然语言和数学语言,具有更高的可读性和可维护性。
高级语言的特点包括:
高级语言具有更强的可读性和可维护性,因为它的语法结构和自然语言更为接近,可以更容易地被人类理解。
高级语言具有更高的抽象能力,可以更容易地表达复杂的算法和逻辑结构,同时也更容易被程序员理解和维护。
高级语言的指令系统通常更为复杂,需要编译器将高级语言代码转换为机器码,因此高级语言的代码通常比较大。
高级语言可以提供更多的功能和特性,例如变量、函数、循环、条件语句、数组、对象等,使得程序编写更加方便和灵活。
高级语言可以更好地支持面向对象编程(OOP)的特性,例如封装、继承、多态等,这使得程序更加模块化和可扩展。
高级语言是一种与具体硬件和操作系统无关的编程语言,它更接近于自然语言和数学语言,具有更高的可读性和可维护性。虽然它的指令系统复杂,但因为它提供了更多的功能和特性,使得程序编写更加方便和灵活。
22、JAVA中有几种基本数据类型,各自占用多少字节呢?
基础类型 | 位数 | 字节 | 封装类 |
---|---|---|---|
int | 32 | 4 | Integer |
short | 16 | 2 | Short |
long | 64 | 8 | Long |
byte | 8 | 1 | Byte |
char | 16 | 2 | Character |
float | 32 | 4 | Float |
double | 64 | 8 | Double |
boolean | Boolean |
以int和Integer为例,对比一下基本数据类型和封装数据类型。
int类型,直接定义一个变量名赋值即可,是Integer需要使用new关键字创建对象;
基本类型和Integer类型混合使用时,Java会自动通过拆箱和装箱实现类型转换;
Integer的默认值是null,而int的默认值是0;
Integer存储在堆内存,int类型是直接存储在栈空间;
Integer作为一个对象类型,封装了一些方法和属性;
23、数据类型之间的如何转换?
在Java中,允许进行数值转换,有些情况会丢失一部分精度。
强制类型转换的一般表示形式为:
double d = 10.2;
int i = (int)d;
System.out.println(i);//10
强制类型转换通过(int)进行double强转int,通过截取小数部分将浮点值转换为整形,会丢失一部分精度。
实线表示无信息丢失的转换。
虚线表示有精度丢失的转换。
24、说说Java中的数据类型提升
Java中的提升是指自动将低精度类型转换为高精度类型的过程。在Java中,当运算符两侧的操作数类型不同时,系统会进行自动类型转换,将低精度类型提升到高精度类型,以避免数据丢失或计算错误。
Java中的基本数据类型按照精度分为以下几类:byte、short、int、long、float和double。
其中,byte和short是最低精度的类型,double是最高精度的类型。
在进行运算时,如果操作数的类型不一致,则系统会自动将较低精度的类型提升为较高精度的类型,以保证运算结果的正确性。
例如,int a = 10; float b = 1.5f; double c = a + b; 在这个例子中,a会被自动提升为float类型,然后与b相加得到一个float类型的结果,最后再自动提升为double类型并赋值给c。
25、Object类有哪些常用方法?
Object类是Javajava.lang包下的核心类,Object类是所有类的父类,何一个类时候如果没有明确的继承一个父类的话,那么它就是Object的子类。
(1)clone()
实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
(2)get class()
final方法,返回 class类型的对象,反射来获取对象。
(3)toString()
toString()方法返回一个字符串。
toString()无处不在,只要对象与一个字符串通过操作符 + 拼接,Java编译器就会自动地调用toString方法来获得这个对象的字符串描述。
还有我们最常用的System.out.println(name)方法,println会自动的调用name.toString(),并打印返回的字符串。
(4)finalize()
该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
(5)equals()
判断内容是否相等,注意,这里比较的不是内存地址。
java语言规范要求equals方法具有下面的特性:
自反性:对于任何非空引用x,x.equals(x)应该返回true;
对称性:对于任何引用x,和y,当且仅当,y.equals(x)返回true,x.equals(y)也应该返回true;
传递性:对于任何引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true;
一致性:如果x,y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果;
对于任意非空引用x,x.equals(null)返回false;
(6)hashCode()
该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
hashcode()方法主要配合基于散列的集合一起使用,比如HashSet、HashMap、HashTable。
当集合需要添加新的对象时,先调用这个对象的hashcode()方法,得到对应的hashcode值,实际上hashmap中会有一个table保存已经存进去的对象的hashcode值,如果table中没有改hashcode值,则直接存入,如果有,就调用equals方法与新元素进行比较,相同就不存了,不同就存入。
(7)wait()
wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生:
其他线程调用了该对象的notify方法;
其他线程调用了该对象的notifyAll方法;
其他线程调用了interrupt中断该线程;
时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
(8)notify()
该方法唤醒在该对象上等待的某个线程。
(9)notifyAll()
该方法唤醒在该对象上等待的所有线程。
26、equals和hashcode的关系
如果equals为true,hashcode一定相等;
如果equals为false,hashcode不一定不相等;
如果hashcode值相等,equals不一定相等;
如果hashcode值不等,equals一定不等;
重写equals方法时,一定要重写hashcode方法
27、String str="i"与 String str=new String(「i」)一样吗?
当使用 String str="i" 时,字符串 "i" 会被存储在字符串常量池中。如果已经存在相同的字符串,则不会创建新的对象,而是返回对该字符串的引用。
当使用 String str=new String("i") 时,会在堆内存中创建一个新的 String 对象,即使字符串常量池中已经存在 "i" 这个字符串。换句话说,每次使用 new 关键字都会创建一个新的对象,而不管字符串常量池中是否已经存在该字符串。
由于 new String("i") 会在堆内存中创建新的对象,这可能会导致内存使用的增加,并且在某些情况下可能会降低性能(例如,当大量创建相同的字符串对象时)。另一方面,使用字符串常量池中的字符串可以减少内存使用并提高性能。
String str="i"是更推荐的写法,因为它可以减少内存使用并提高性能。
28、String s = "nezha";s = s + " soft";这两行代码执行后,原始的 String 对象中的内容到底变了没有?
这两行代码执行后,原始的 String 对象中的内容没有变。在 Java 中,String 是不可变的,这意味着一旦创建了一个 String 对象,就不能改变它的内容。当你使用 "+" 运算符连接两个字符串时,实际上是创建了一个新的 String 对象,然后将两个字符串的内容复制到新的对象中。所以,原始的 String 对象("nezha")并没有被修改,而是创建了一个新的 String 对象("nezha soft"),并将变量 s 指向这个新的对象。
29、如何将字符串反转?
使用StringBuilder的reverse()方法。
public classReverseString{
publicstaticvoidmain(String[] args){
String originalString = "Hello, World!";
StringBuilder sb = new StringBuilder(originalString);
String reversedString = sb.reverse().toString();
System.out.println(reversedString); // 输出: "!dlroW ,olleH"
}
}
30、String 类的常用方法都有那些?
(1)常见String类的获取功能
length:获取字符串长度;
charAt(int index):获取指定索引位置的字符;
indexOf(int ch):返回指定字符在此字符串中第一次出现处的索引;
substring(int start):从指定位置开始截取字符串,默认到末尾;
substring(int start,int end):从指定位置开始到指定位置结束截取字符串;
(2)常见String类的判断功能
equals(Object obj):比较字符串的内容是否相同,区分大小写;
contains(String str): 判断字符串中是否包含传递进来的字符串;
startsWith(String str): 判断字符串是否以传递进来的字符串开头;
endsWith(String str): 判断字符串是否以传递进来的字符串结尾;
isEmpty(): 判断字符串的内容是否为空串"";
(3)常见String类的转换功能
byte[] getBytes(): 把字符串转换为字节数组;
char[] toCharArray(): 把字符串转换为字符数组;
String valueOf(char[] chs): 把字符数组转成字符串。valueOf可以将任意类型转为字符串;
toLowerCase(): 把字符串转成小写;
toUpperCase(): 把字符串转成大写;
concat(String str): 把字符串拼接;
(4)常见String类的其他常用功能
replace(char old,char new) 将指定字符进行互换
replace(String old,String new) 将指定字符串进行互换
trim() 去除两端空格
int compareTo(String str) 会对照ASCII 码表 从第一个字母进行减法运算 返回的就是这个减法的结果,如果前面几个字母一样会根据两个字符串的长度进行减法运算返回的就是这个减法的结果,如果连个字符串一摸一样 返回的就是0。
31、String s = new String("nezha");创建了几个字符串对象?
第一次调用时,会在堆内存中创建一个字符串对象,同时在字符串常量池中创建一个对象「nezha」;第二次调用时,只会在堆内存中创建一个字符串对象,指向之前在字符串常量池中创建的对象「nezha」。
32、想新建一个java.lang.String类,能建成功吗?这个类会被类加载器加载吗?为什么?
不能成功新建一个名为java.lang.String的类,这个类也不会被类加载器加载,因为这样做违反了Java的命名规范和类加载机制。
33、String类可以被继承吗?
在Java中,String类是一个被声明为final的类。由于final关键字的特性,String类不能被继承。这意味着你不能创建String类的子类。这种设计决策是为了确保String类的行为在Java中始终如一,防止由于继承可能引入的不可预知的行为。
final类在Java中有以下特点:
它不能被继承。
它不能有子类。
尝试创建final类的子类会导致编译错误。
因此,由于String类是final的,你不能创建它的子类。在需要自定义字符串行为的情况下,你可以考虑使用其他方式,例如创建包含String对象的新类,并在其中实现所需的行为。
34、String,Stringbuffer,StringBuilder 的区别?
从4个角度进行对比:
(1)可变性
String内部的value值是final修饰的,所以它是不可变类。所以每次修改String的值,都会产生一个新的对象。
StringBuffer和StringBuilder是可变类,字符串的变更不会产生新的对象。
(2)线程安全性
String是不可变类,所以它是线程安全的。
StringBuffer是线程安全的,因为它每个操作方法都加了synchronized同步关键字。
StringBuilder不是线程安全的,所以在多线程环境下对字符串进行操作,应该使用StringBuffer,否则使用StringBuilder。
(3)性能方面
String的性能是最低的,因为String是不可变的,这就意味着在做字符串拼接和修改的时候,需要重新创建新的对象以及分配内存。
StringBuffer因为是可变的,直接修改即可,但StringBuffer添加了重量级锁synchronized,其性能不如StringBuilder。
(4)存储方面
String存储在字符串常量池中。
StringBuilder和StringBuffer都存储在堆内存中。
35、「+」连接符的效率为何低?
使用「+」连接符时,JVM会隐式的创建StringBuilder对象,这种方式在大部分情况下不会造成效率的损失,但是,在循环中进行字符串拼接时就不一样了。
因为会创建大量的StringBuilder对象在堆内存中,这肯定是不允许的,所以这时就建议在循环外创建一个StringBuilder对象,然后循环内调用append方法进行手动拼接。
还有一种特殊情况,如果「+」拼接的是字符串常量中的字符串时,编译器会进行优化,直接将两个字符串常量拼接好。
所以,「+」连接符对于直接相加的字符串常量效率很高,因为在编译期间便确定了它的值;但对于间接相加的情况效率就会变低,建议单线程时使用StringBuilder,多线程时使用StringBuffer替代。
36、说说缓冲区数据结构bytebuffer
缓冲区是由具有相同类型的数值构成的数组,Buffer是一个抽象类,它有很多子类,包括ByteBuffer、CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ShortBuffer。
每个缓冲区都具有4个属性:
容量capacity,缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,且永远不能被改变;
读写位置position,下一个要被读或写的元素的索引。位置会自动由相应的 get() 和 put() 函数更新。这里需要注意的是positon的位置是从0开始,比如,已经写入buffer 3个元素那那么position就是指向第4个位置,即position设置为3(数组从0开始计);
界限limit,缓冲区的第一个不能被读或写的元素。缓冲区创建时,limit 的值等于 capacity 的值。假设 capacity = 1024,我们在程序中设置了 limit = 512,说明Buffer 的容量为 1024,但是从 512 之后既不能读也不能写,因此可以理解成,Buffer 的实际可用大小为 512;
可选的标记 mark,标记,一个备忘位置。保存某个时刻的position指针的值,通过调用mark()实现,当mark被置为负值时,表示废弃标记。标记在设定前是未定义的(undefined)。使用场景是,假设缓冲区中有 10 个元素,position 目前的位置为 2(也就是如果get的话是第三个元素),现在只想发送 6 - 10 之间的缓冲数据,此时我们可以 buffer.mark(buffer.position());即把当前的 position 记入 mark 中,然后 buffer.postion(6);此时发送给 channel 的数据就是 6 - 10 的数据。发送完后,我们可以调用 buffer.reset() 使得 position = mark,因此这里的 mark 只是用于临时记录一下位置用的。
position和limit之间的距离指定了可读/写的字节数。
-1 <= mark <= position <= limit <= capacity 0<= position <= limit <= capacity
使用缓冲区的主要目的是执行读写循环操作。
假设我们有一个缓冲区,在一开始,它的位置是0,界限等于容量。我们不断地调用put将值添加到这个缓冲区中,当我们耗尽所有的数据或者写出的数据量达到容量大小时,就该切入到读操作了。
这时可以调用flip方法将界限设置到当前位置,并把位置复位到0.现在在remaining方法返回正数时(它返回的值是界限 - 位置),不断地调用get。在我们将缓冲区中所有的值都写入之后,调用clear使缓冲区为下一次写循环做好准备。clear方法将位置复位到0,并将界限复位到容量。
如果想重读缓冲区,可以使用rewind或mark/reset进行复位。
然后可以 用某个通道的数据填充缓冲区,或者将缓冲区的内容写出到通道中。
ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);
Buffer及其子类都不是线程安全的,若多线程操作该缓冲区,则应通过同步来控制对该缓冲区的访问。
37、hashcode是什么?有什么作用?
hashCode主要用于获取对象的哈希码值。
这个哈希码值是一个整数,主要用于数据结构(如哈希表)中,以快速定位对象的位置。
Java 的集合框架(如 HashSet、HashMap 等)在内部使用了哈希表来存储元素,这些集合在添加、删除或查找元素时,都会使用到元素的 hashCode。当我们需要查找一个对象时,哈希表会使用该对象的 hashCode 来计算对象在哈希表中的位置;
当要对对象进行比较时,可以先比较两对象的hashcode值
如果hashcode值相等,则再用equals进行比较;
如果hashcode值不等,则无需再进行equals比较,两对象一定不等。
38、Java 创建对象有几种方式
使用new关键字,这是最常见的创建对象的方式。通过调用类的构造方法(构造器)来创建对象。
使用反射,通过Java的反射API可以动态地创建对象。反射允许在运行时获取类的信息,并可以调用类的构造器来创建对象。
使用克隆,如果一个类实现了Cloneable接口并重写了Object类的clone()方法,那么可以通过调用对象的clone()方法来创建该对象的一个副本。
使用序列化与反序列化,如果一个类实现了Serializable接口,那么可以通过序列化(将对象转换为字节流)和反序列化(将字节流转换回对象)来创建对象。这种方式常用于对象的持久化存储和传输。
使用依赖注入,在依赖注入框架(如Spring)中,对象的创建和管理通常由框架负责。通过配置或注解,框架会自动创建所需的对象,并将其注入到需要的地方。
使用工厂模式,工厂模式是一种创建对象的设计模式,它隐藏了对象创建的具体逻辑,并通过一个统一的接口来创建对象。工厂模式可以分为简单工厂、工厂方法和抽象工厂等。
使用构建器模式,构建器模式(Builder Pattern)是一种对象构建的设计模式,它允许你以更加灵活的方式创建复杂对象。构建器模式通常用于构造具有多个可选参数的类。
39、说说对象创建的过程
① 创建对象时,或者第一次访问类的静态方法或静态字段时,Java解释器会搜索类路径来定位 class文件。
② 当 class被加载后,将创建一个 class对象,它的所有静态初始化工作都会执行。
因此,静态初始化只在 class对象首次加载时发生一次。
③ 当使用new创建对象时,首先会在堆上为对象分配足够的存储空间,这块存储空间会被清空,然后自动将对象中的所有基本类型设置为其默认值,比如int的默认值为0,Integer的默认值为null。
④ 执行所有初始化操作。
⑤ 执行构造器。
40、对象间的四种关系
(1)依赖
依赖关系表示一个类依赖于另一个类的定义。例如,一个人(Person)可以买车(car)和房子(House),Person类依赖于Car类和House类的定义,因为Person类引用了Car和House。与关联不同的是,Person类里并没有Car和House类型的属性,Car和House的实例是以参量的方式传入到buy()方法中去的。一般而言,依赖关系在Java语言中体现为局域变量、方法的形参,或者对静态方法的调用。
(2)关联
关联(Association)关系是类与类之间的联接,它使一个类知道另一个类的属性和方法。关联可以是双向的,也可以是单向的。在Java语言中,关联关系一般使用成员变量来实现。
(3)聚合
聚合(Aggregation) 关系是关联关系的一种,是强的关联关系。聚合是整体和个体之间的关系。例如,汽车类与引擎类、轮胎类,以及其它的零件类之间的关系便整体和个体的关系。与关联关系一样,聚合关系也是通过实例变量实现的。但是关联关系所涉及的两个类是处在同一层次上的,而在聚合关系中,两个类是处在不平等层次上的,一个代表整体,另一个代表部分。
(4)组合
组合(Composition) 关系是关联关系的一种,是比聚合关系强的关系。它要求普通的聚合关系中代表整体的对象负责代表部分对象的生命周期,组合关系是不能共享的。代表整体的对象需要负责保持部分对象和存活,在一些情况下将负责代表部分的对象湮灭掉。代表整体的对象可以将代表部分的对象传递给另一个对象,由后者负责此对象的生命周期。换言之,代表部分的对象在每一个时刻只能与一个对象发生组合关系,由后者排他地负责生命周期。部分和整体的生命周期一样。
41、说说隐式参数和显式参数
显式参数是我们在调用方法时明确传递的参数。这些参数在方法声明中作为方法签名的一部分出现,并且在调用时需要提供具体的值。
隐式参数不是由程序员明确传递的,而是系统自动传递的。最常见的例子就是this关键字,它引用了当前对象本身。
在非静态方法中,我们可以使用this关键字来引用当前对象的属性或方法。这个this就是隐式参数,因为它不需要我们显式地传递给方法,但方法内部可以访问它。
另一个隐式参数是静态方法中的 class参数,它指向定义该方法的类,可以通过 className. class的形式获取,这也是一种隐式参数。
42、说说Java参数可变
可变参数是Java 5中引入的一个特性,它允许方法接受任意数量的参数,这些参数在方法内部被当作数组处理。
在方法声明中,使用三个点...来表示可变参数。这意味着该方法可以接收任意数量的指定类型的参数。在方法内部,这些参数被当作数组处理。
可变参数可以用于方法重载,但需要注意的是,如果一个方法使用了可变参数,那么它不能与只接受单个参数的方法进行重载。在重写时,子类方法也可以使用可变参数,但需要保持参数类型的兼容性。
可变参数一般用于日志记录,数学计算等场景,但过度使用可变参数可能会导致代码难以理解和维护,因此需要谨慎使用。
43、普通类和抽象类有哪些区别?
实例化:普通类可以被实例化,即可以创建这个类的对象。然而,抽象类不能被实例化。尝试实例化抽象类会导致编译错误。这是因为抽象类通常表示一种概念或行为,而不是具体的对象。
抽象方法:抽象类可以包含抽象方法,这是没有实现的方法,只有方法的声明,没有具体的实现。普通类则不能包含抽象方法。如果一个类包含抽象方法,那么这个类必须被声明为抽象类。
继承:一个类可以从一个抽象类继承,但也可以从普通类继承。然而,一个抽象类只能被另一个类继承,而不能被实例化。这意味着抽象类是类的基类,用于定义一些通用的属性和方法,而具体的实现则由子类来完成。
设计目的:普通类通常用于表示具体的实体或对象,如一个具体的动物或人。而抽象类则用于表示一种概念或一组具有共同特性的对象,这些对象的具体实现由子类来完成。例如,一个"动物"类可能是一个抽象类,而"狗"和"猫"则可能是这个抽象类的具体实现。
总的来说,普通类和抽象类在实例化、包含抽象方法、继承和设计目的等方面存在明显的差异。抽象类提供了一种方式来定义一组具有共同特性的对象的概念,而具体的实现则由子类来完成。这使得抽象类在面向对象编程中非常有用,尤其是在需要创建一组具有共同行为的对象时。
44、接口和抽象类有什么区别?
(1)定义与实现
接口是一种抽象类型,它定义了一组方法的规范,但不提供具体的实现。接口中的所有方法都是抽象的,没有方法体。接口主要用于定义对象的行为。
抽象类是一种不能被实例化的类,它定义了一组抽象方法和非抽象方法。抽象方法没有具体实现,需要子类来提供实现。抽象类主要用于定义对象的共同属性和行为,并作为子类的基类。
(2)继承与实现
一个类可以实现多个接口,这意味着一个类可以拥有多个行为规范。
一个类只能继承自一个抽象类,子类继承抽象类时,必须提供抽象类中所有抽象方法的具体实现。
(3)字段与属性
接口中不能定义字段,只能定义常量(静态的、不可变的)。
抽象类中可以定义字段、常量、抽象方法以及非抽象方法。
45、default方法是什么?
通过default关键字修饰的方法就是默认方法。
如果接口中有很多方法,实现它的类就需要重写接口中的所有方法,不管是否需要用到。如果接口中的某个方法被default关键字修饰了,那么具体的实现类中可以不用实现方法。
46、说说Java中多态的实现原理
多态机制包括静态多态(编译时多态)和动态多态(运行时多态)。
静态多态比如说重载,动态多态一般指在运行时才能确定调用哪个方法。
我们通常所说的多态一般指运行时多态,也就编译时不确定究竟调用哪个具体方法,一直等到运行时才能确定。
多态实现方式:继承extends和实现implements。
多态的核心在于子类对父类方法的改写或对接口方法的实现,以取得在运行时不同的执行效果。
当调用对象的某个方法时,JVM查找该对象类的方法表,以确定该方法的直接引用地址,有了地址后才真正调用该方法。
47、构造器可以被重写吗?
构造器在面向对象编程中用于初始化对象的状态。构造器不是传统意义上的方法,因此它们不能被重写。重写是子类对父类中已有的方法进行重新定义的过程,使得子类对象在调用该方法时执行的是子类中的定义,而不是父类中的定义。
然而,构造器在子类中可以被调用,并且子类可以定义自己的构造器。这通常是通过在子类的构造器中调用父类的构造器来实现的,使用super()关键字。这并不意味着构造器被重写,而是子类构造器在初始化对象时会包含父类构造器的初始化逻辑。
48、说说Java Bean的命名规范
(1)类名
首字母大写,并且符合驼峰命名法,禁用拼音。
(2)成员变量
成员变量应该是private私有的,以确保封装性。
属性的命名也应该采用驼峰命名法,并且首字母小写。
每一个成员变量都有一对公有的Getter和Setter方法,以便外部代码可以访问和修改属性的值。
如果属性是布尔类型,可以使用is代替get作为前缀,例如isVisible。
(3)构造方法
JavaBean应该提供一个无参数的默认构造函数,这是为了使JavaBean在通过反射实例化时能够正常工作。如果需要,也可以提供带参数的构造函数,以便在创建Bean实例时初始化其属性。
(4)其它注意事项
避免使用Java保留字和特殊字符作为类名、属性名或方法名。
在命名时,应考虑到代码的可读性和可维护性,避免使用过于复杂或难以理解的名称。
保持命名的一致性,例如在整个项目中,如果某个特定的概念或对象使用了特定的命名方式,那么在其他地方也应遵循相同的命名方式。
49、Java内部类是什么,有哪些应用场景?
Java内部类是定义在另一个类中的类。
内部类可以访问外部类的所有成员(包括私有成员),并且可以隐藏外部类的某些成员。内部类主要有四种类型:静态内部类、成员内部类、局部内部类和匿名内部类。
应用场景:
每个内部类都可以独立的继承一个类,所以无论外部类是否继承了某个类,内部类依然可以继承其他类,这就完美的解决了java没有多继承的问题。
可以有效的将有一定关系的类组织在一起,又可以对外界有所隐藏。
方便编写事件驱动程序
方便编写多线程代码
用来装逼,代码越复杂,是不是感觉越牛逼。
50、静态内部类与非静态内部类有什么区别
定义位置与访问权限:静态内部类定义在类的内部,使用static关键字进行定义。它只能访问外部类的静态成员变量和方法,而不能访问外部类的非静态成员。非静态内部类可以定义在外部类的任意位置,包括方法内部(此时称为局部内部类)。非静态内部类可以直接访问外部类的所有成员,包括私有成员。
创建实例的方式:生成一个静态内部类不需要外部类成员,静态内部类的对象可以直接生成。而对于非静态内部类,其创建依赖于外部类的实例,不能独立存在,即需要有一个外部类的实例来引用它。
应用场景:静态内部类由于其不能访问外部类的非静态成员,因此在某些情况下,如仅需要访问外部类的静态成员时,使用静态内部类会更为合适。非静态内部类则因其可以访问外部类的所有成员,包括私有成员,所以在需要这种访问权限的场景下更为常用。
51、throw 和 throws 的区别?
throw代表一个动作,用于在程序中明确地抛出一个异常对象。它出现在方法体内部,可以作为单独的语句使用。throw用于表示一个具体的异常类型,其后面必须跟一个异常实例。
throws:代表一种状态,用于声明一个方法可能会抛出的异常。它出现在方法声明中,跟在方法参数列表后面,不能单独使用。throws用于声明在该方法内可能抛出的异常,但并不抛出具体的异常实例。
52、final、finally、finalize 有什么区别?
final可以修饰类,变量,方法,修饰的类不能被继承,修饰的变量不能重新赋值,修饰的方法不能被重写
finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。
finalize方法用于垃圾回收。
一般情况下不需要我们实现finalize,当对象被回收的时候需要释放一些资源,比如socket链接,在对象初始化时创建,整个生命周期内有效,那么需要实现finalize方法,关闭这个链接。
但是当调用finalize方法后,并不意味着gc会立即回收该对象,所以有可能真正调用的时候,对象又不需要回收了,然后到了真正要回收的时候,因为之前调用过一次,这次又不会调用了,产生问题。所以,不推荐使用finalize方法。
53、trycatch的执行顺序
从try中第一行代码开始执行,执行到出现异常的代码,JVM会创建一个异常对象。
判断catch是否能捕获到jvm创建的异常对象,① 如果捕获到就跳到catch代码块中,不会结束程序,继续从catch中的代码逻辑;② 如果捕获不到,直接打印异常信息并结束程序。
如果try中没有异常,则执行完try中代码,跳过catch,进入finally代码块。
54、try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
55、常见的异常类有哪些?
NullPointerException:空指针异常;
SQLException:数据库相关的异常;
IndexOutOfBoundsException:数组下角标越界异常;
FileNotFoundException:打开文件失败时抛出;
IOException:当发生某种IO异常时抛出;
classCastException:当试图将对象强制转换为不是实例的子类时,抛出此异常;、
NoSuchMethodException:无法找到某一方法时,抛出;
ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常;
NumberFormatException:当试图将字符串转换成数字时,失败了,抛出;
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数「除以零」时,抛出此类的一个实例。
56、开发中,你是如何处理异常的?
方法内如果抛出需要检测的异常,那么方法上必须要声明,否则必须在方法内用try-catch捕捉,否则编译失败。如果调用了声明异常的函数,要么try-catch要么throws,否则编译失败。
什么时候catch,什么时候throws?功能内容可以解决,用catch,解决不了,用throws告诉调用者,有调用者解决。如果一个功能抛出了多个异常,那么调用时必须有对应多个catch进行针对性的处理。
57、No classDefFoundError 和 classNotFoundException 有什么区别?
No classDefFoundError是JVM运行时通过 classpath加载类时,找不到对应的类而抛出的错误。
classNotFoundException:如果在编译过程中可能出现此异常,在编译过程中必须将其抛出。
No classDefFoundError的发生场景:
类依赖的 class或jar不存在
类文件存在,但是在不同的域中,简而言之,就是找不到
classNotFoundException的发生场景:
调用 class的forName方法时,找不到指定的类
classLoader中的findSystem class() 方法时,找不到指定的类
58、聊聊记录日志的规范
(1)日志的可读性,日志是记录问题的,然后让维护人员看的,规范的、通俗易懂的日志才是王道。
(2)日志的性能,不管是记录到文件里,还是记录到数据库里,记录日志肯定是要消耗程序性能的,这样,哪些需要记下,哪些不用记,需要权衡利弊。
(3)大的循环中,尽量不要记录日志。
(4)日志的级别,一般情况下,程序运行时记录info日志,发生异常时记录error日志,也可以记录警告日志,比如某些参数超过了,但是不影响整体程序的运行。
59、哪些内容需要记在日志里?
哪些内容需要记录在日志中,日志不是越多越好,越丰满越好,凡是要有一个度。
日期;
时间;
日志级别;
代码位置;
线程号;
日志内容;
错误码;
错误信息
业务描述;
关键字,比如产品id;
入参、回参;
总之一句话,你认为哪些参数比较重要,有助于你排查问题,就记录哪些。
60、Log4j有哪些日志级别?
Log4j 是 Apache 的一个开源项目,项目中一般都是通过Log4j来记录日志,可以通过配置文件定义日志输出的级别、格式、存储路径等。
Log4j 中将要输出的 Log 信息定义了 6 种级别,依次为 TRACE、DEBUG、INFO、WARN、ERROR 和 FATAL。
(1)TRACE
很低的日志级别,一般不会使用。
(2)DEBUG
一般在项目调试阶段使用,记录的日志更细粒化,主要打印开发过程中的一些重要变量。
(3)INFO
info日志,是最常用的日志,用于记录正常运行情况下,程序的执行情况,执行轨迹,打印一些比较重要的东西,但不能滥用,避免日记记录过多,维护运维阶段定位问题过于麻烦。
(4)WARN
主要用于记录一些警告,比如你的本意是查询某些产品信息,但是没有查到,逻辑上没有错误,但业务上说不通。
(5)ERROR
通常在发生异常、程序入参校验失败时,用error级别记录,也会单独产生error文件。
(6)FATAL
FATAL 指出每个严重的错误事件将会导致应用程序的退出,这个级别比较高,重大错误,程序无法恢复,必须通过重启程序来解决。
61、什么是 java 序列化?什么情况下需要序列化?
序列化就是把内存里面的对象转化为字节流,以便用来实现存储或者传输。
序列化的前提是保证通信双方对于对象的可识别性,所以很多时候,我们会把对象先转化为通用的解析格式,比如json、xml等。然后再把他们转化为数据流进行网络传输,从而实现跨平台和跨语言的可识别性。
序列化是通过实现serializable接口,该接口没有需要实现的方法,implement Serializable只是为了标注该对象是可被序列化的。
反序列化就是根据从文件或者网络上获取到的对象的字节流,根据字节流里面保存的对象描述信息和状态。
62、序列化使用场景有哪些?
像银行卡、密码这些字段不能被序列化;
将对象存储到文件中时进行序列化,从文件中读取对象时需要反序列化;
分布式传递对象,或者网络传输,需要序列化;
存入缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
63、使用序列化和反序列化的注意事项
(1)Java序列化的方式
实现 Serializable 接口:可以自定义 writeObject、readObject、writeReplace、readResolve 方法,会通过反射调用。
实现 Externalizable 接口,它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。因为序列化哪些字段,需要方法指定,所以transient在这里无效。
(2)序列化ID问题
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。
(3)静态字段不会序列化
序列化时不保存静态变量,这是因为序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
(4)transient
transient代表对象的临时数据。
如果你不想让对象中的某个成员被序列化可以在定义它的时候加上 transient 关键字进行修饰
,这样,在对象被序列化时其就不会被序列化。
transient 修饰过的成员反序列化后将赋予默认值,即 0 或 null。
有些时候像银行卡号这些字段是不希望在网络上传输的,transient的作用就是把这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
(5)父类的序列化
当一个父类实现序列化,子类自动实现序列化;而子类实现了 Serializable 接口,父类也需要实现Serializable 接口。
(6)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化
(7)并非所有的对象都可以序列化
① 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的;
② 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现;
(8)序列化解决深拷贝问题
如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存,这是能用序列化解决深拷贝的重要原因。
64、为什么要使用克隆?如何实现对象克隆?深拷贝和浅拷贝区别是什么?
(1)什么要使用克隆?
想对一个对象进行复制,又想保留原有的对象进行接下来的操作,这个时候就需要克隆了。
(2)如何实现对象克隆?
实现Cloneable接口,重写clone方法;
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆;
BeanUtils,apache和Spring都提供了bean工具,只是这都是浅克隆。
(3)深拷贝和浅拷贝区别是什么?
浅拷贝,指的是重新分配一块内存,创建一个新的对象,指针指向被复制对象的同一块内存地址,如果原对象发生改变,那么浅拷贝得到的新对象也会反映出这些变化;
深拷贝:它是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。这意味着深拷贝不仅复制了对象本身,还复制了对象所引用的所有其他对象。因此,新对象和原对象没有任何关联,对其中一个对象的修改不会影响另一个对象。
65、Java反射是什么?有哪些应用场景
Java反射是Java语言的一个核心特性,它允许程序在运行时检查类、接口、字段和方法的信息,并能动态地调用对象的方法。反射的主要作用是增强程序的灵活性,使得程序能够在运行时动态地加载、链接和使用类。
但反射的代码比正常调用的代码更多,性能更慢,应避免使用反射。
Java反射应用场景:
在框架设计中,反射常被用于实现框架的扩展性和灵活性。框架可以通过反射在运行时加载和使用不同的类,从而实现对不同业务逻辑的支持;
通过反射,可以创建动态代理对象,这些代理对象可以在运行时代表其他对象执行操作。这在实现AOP(面向切面编程)等高级功能时非常有用;
反射可以解析注解信息,并执行相应的操作;
在如Hibernate这样的ORM框架中,对象需要被转化为实体类并存储到数据库中。这个过程中,反射被用于动态创建实体类对象、获取类的属性和方法等;
在Java中,序列化和反序列化都需要使用到反射技术。序列化会将对象转化成字节流,反序列化则将字节流还原为对象。在这个过程中,需要借助反射技术来获取对象的属性信息。
Java反射提供了一种强大的机制,使得程序能够在运行时动态地操作类和对象,从而增强了程序的灵活性和可扩展性。然而,反射也有一些潜在的性能开销和安全性问题,因此在使用时需要谨慎考虑。
66、java 中都有哪些引用类型?
(1)强引用
Java中默认声明的就是强引用,比如:
Object obj = new Object();
obj = null;
只要强引用存在,垃圾回收器将永远不会回收被引用的对象。如果想被回收,可以将对象置为null;
(2)软引用(SoftReference)
在内存足够的时候,软引用不会被回收,只有在内存不足时,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会跑出内存溢出异常。
byte[] buff = newbyte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
(3)弱引用(WeakReference)
进行垃圾回收时,弱引用就会被回收。
(4)虚引用(PhantomReference)
(5)引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用、虚引用一起配合使用。
当垃圾回收器准备回收一个对象时,如果发现它还有引用,就会在回收对象之前,把这个引用加入到引用队列中。
程序可以通过判断引用队列中是否加入了引用,来判断被引用的对象是否将要被垃圾回收,这样可以在对象被回收之前采取一些必要的措施。
67、枚举是什么,有哪些特点?
枚举作为一个类,可以有自己的属性(通常应该是常量,我没遇到过不是的情况)以及自己的方法(否则只能用switch来写,实际违反原则)
枚举类型检查,有效性检查
和常量相比,无需查看文档和源码就能直接知道所有可能返回值,方便编码。
与switch配合使用解决ifelse过多的问题,使用switch进行条件判断时,条件参数一般只能是整型,字符型。而枚举型确实也被switch所支持,在java 1.7后switch也对字符串进行了支持。
68、泛型是什么,有哪些应用场景?
泛型是一种编程范式,其核心思想是将类型参数化,即把数据类型作为参数传递。通过泛型,可以创建灵活且可重用的代码,而无需针对每种数据类型都编写重复的代码。泛型的主要优点包括提高代码的可读性、可维护性和类型安全性。
应用场景:
集合类和数据结构:泛型最常见的用途是在集合类(如ArrayList、LinkedList、HashMap等)和数据结构中使用。通过使用泛型,可以创建一个通用的集合类,用于存储不同类型的元素,并在编译时捕获类型错误。
自定义数据结构:使用泛型可以创建自定义的数据结构,以适应不同类型的数据。这样可以编写通用的、可重用的代码,减少为不同类型的数据编写不同实现的需求。
泛型方法:除了泛型类,还有泛型方法,即在方法级别使用泛型。这对于那些只需要在特定方法中使用泛型的情况非常有用。
接口和抽象类:泛型也可以用于接口和抽象类的定义,以创建通用的接口和抽象类,这些接口和类可以被不同类型的实现或子类使用。
数据库操作:在数据库操作中,泛型可以用于定义数据库表中各个字段的类型,从而提高程序的类型安全性。
使用泛型的主要目的之一是提供编译时类型检查,这有助于减少在运行时出现类型错误的可能性。此外,泛型还可以提高代码的可读性和重用性,避免不必要的类型转换和强制类型转换,从而提高代码的安全性和可维护性。
69、java 中 IO 流分为几种?
(1)字节流
在 I/O 操作中,数据被视为一系列按顺序排列的字节流。在 Java 中,这种字节流被称为 InputStream 和 OutputStream。
InputStream 代表一个输入流,它是一个抽象类,不能被实例化。InputStream 定义了一些通用方法,如 read() 和 skip() 等,用于从输入流中读取数据。
OutputStream 代表一个输出流,它也是一个抽象类,不能被实例化。OutputStream 定义了一些通用方法,如 write() 和 flush() 等,用于向输出流中写入数据。
(2)字符流
除了字节流,Java 还提供字符流,字符流类似于字节流,不同之处在于字符流是按字符读写数据,而不是按字节。Java 中最基本的字符流是 Reader 和 Writer,它们是基于 InputStream 和 OutputStream 的转换类,用于完成字节流与字符流之间的转换。
(3)缓冲流
BufferedInputStream 和 BufferedOutputStream 是 I/O 包中提供的缓冲输入输出流。它们可以提高 I/O 操作的效率,具有较好的缓存机制,能够减少磁盘操作,缩短文件传输时间。使用 BufferedInputStream 和 BufferedOutputStream 进行读取和写入时,Java 会自动调整缓冲区的大小,使其能够适应不同的数据传输速度。
(4)对象流
可以读取或写入 Java 对象的流,比较典型的对象流包括ObjectInputStream 和 ObjectOutputStream。
对象流需要将对象序列化和反序列化为字节序列,使用 ObjectInputStream 和 ObjectOutputStream 可以将 Java 对象转换为字节流进行传输或存储。
在网络传输和文件存储中,ObjectInputStream 和 ObjectOutputStream 通常会被使用到。
70、BIO、NIO、AIO 有什么区别?
(1)同步阻塞BIO
JDK1.4之前,建立网络连接的时候采用BIO模式,先在启动服务端socket,然后启动客户端socket,对服务端通信,客户端发送请求后,先判断服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话会等待请求结束后才继续执行。
线程发起IO 请求,不管内核是否准备好IO 操作,从发起请求起,线程一直阻塞,直到操作完成。
服务器实现模式为 一个连接一个线程 ,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。
(2)同步非阻塞NIO
NIO主要是想解决BIO的大并发问题,BIO是每一个请求分配一个线程,当请求过多时,每个线程占用一定的内存空间,服务器瘫痪了。
JDK1.4开始支持NIO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中。
线程发起IO 请求,立即返回;内核在做好IO 操作准备之后,通过调用注册回调函数通知线程做IO操作,线程开始阻塞,直到操作完成。
服务器实现模式为 一个请求一个线程 ,即客户端发送连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O 请求时才启动一个线程进行处理。
(3)异步非阻塞AIO
JDK1.7开始支持AIO,适用于连接数目多且连接比较长的结构,比如相册服务器,充分调用OS参与并发操作。
线程发起IO 请求,立即返回;内存做好IO 操作准备之后,做IO 操作,直到操作完成或者失败,通过调用注册回调函数通知线程做IO操作完成或者失败。
服务器实现模式为 一个有效请求一个线程 ,客户端IO 请求都由OS先完成了再通知服务器应用去启动线程进行处理。
71、BIO、NIO、AIO有哪些应用场景
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕 系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分 调用OS参与并发操作,编程比较复杂,JDK7开始支持
72、简述一下BIO的编程流程
服务器端启动一个ServerSocket;
客户端启动Socket对服务器进行通 信,默认情况下服务器端需要对每 个客户 建立一个线程与之通讯;
客户端发出请求后, 先咨询服务器 是否有线程响应,如果没有则会等 待,或者被拒绝;
如果有响应,客户端线程会等待请 求结束后,在继续执行;
73、NIO的三大核心部分是什么?
Selector(选择器)、Channel(通道)、Buffer(缓冲区)。
Selector 对应一个线程, 一个线程对应多个channel(连接);
该图反应了有三个channel 注册到 该selector //程序;
每个channel 都会对应一个Buffer;
程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念;
Selector 会根据不同的事件,在各个通道上切换;
Buffer 就是一个内存块 , 底层是有一个数组;
数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是 输出流, 不能双向,但是NIO的Buffer 是可以读也可以写, 需要 flip 方法切换;
channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统 通道就是双向的;
NIO是面向缓冲区,或者说面向块编程,数据读取到一个 它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就 增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求 的数量比HTTP1.1大了好几个数量级。
缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个 容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对 象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
74、NIO中buffer的四大属性是什么?
mark:标记
position:位置,下一个要被读或写的元素的索引, 每次读写缓冲区数据时都会改变改值, 为下次读写作准备。
limit:表示缓冲区的当前终点,不能对缓冲区 超过极限的位置进行读写操作。且极限 是可以修改的
capacity:容量,即可以容纳的最大数据量;在缓 冲区创建时被设定并且不能改变。
75、对比一下BIO和NIO?
BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多;
BIO 是阻塞的,NIO 则是非阻塞的;
BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进 行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因 此使用单个线程就可以监听多个客户端通道。
76、FileChannel是做什么的?
FileChannel主要用来对本地文件进行 IO 操作,常见的方法有:
read,从通道读取数据并放到缓冲区中
write,把缓冲区的数据写到通道中
transferFrom,从目标通道 中复制数据到当前通道
transferTo,把数据从当 前通道复制给目标通道
77、简述一下Selector选择器
Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连 接,就会使用到Selector(选择器)。
Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然 后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个 通道,也就是管理多个连接和请求。
只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少 了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
避免了多线程之间的上下文切换导致的开销。
78、lambda表达式是什么,有哪些应用场景?
在Java 8中,Lambda表达式被引入作为一个重要新特性,使得Java能够进行函数式编程,并在并发性能上取得了实质性的进步。Lambda表达式简化了匿名内部类的形式,并达到了同样的效果,使得代码更加简洁。
应用场景:
集合操作:Lambda表达式可以与集合操作方法(如forEach、filter、map、reduce)结合使用,对集合中的元素进行遍历、筛选、映射、聚合等操作。
排序:Lambda表达式可以用于自定义的排序功能,通过传递不同的比较规则实现对集合中元素的排序。
线程编程:Lambda表达式可以简化线程编程中的代码,例如使用Lambda表达式创建Runnable对象,或使用Lambda表达式实现函数式接口来处理线程任务。
GUI事件处理:Lambda表达式可用于简化GUI事件处理代码,如为按钮、菜单等组件注册事件监听器。
数据处理:对于大数据集的处理,如统计、过滤、转换等,Lambda表达式表现出色。其并行处理的能力可以提高数据处理的效率。
Web开发:Lambda表达式可以简化Web开发中的重复性代码,例如通过Lambda表达式实现控制器、过滤器、拦截器等。
79、Java8 ::是什么,有哪些应用场景?
在Java 8中,双冒号「::」是一个方法引用操作符,也被称为方法引用符。它用于引用类的方法,并返回一个函数接口(function interface)。这与lambda表达式有所不同,因为lambda表达式需要自定义一个lambda体,而「::」则直接引用一个已存在的方法。
「::」操作符在Java 8中有多种用法:
引用静态方法:使用「类名::静态方法名」的格式。例如,Integer::parseInt就是引用Integer类的parseInt静态方法。
引用对象方法:使用「实例对象::实例方法」的格式。例如,对于字符串的substring方法,可以写成String::substring。
引用构造方法:使用「类名::new」的格式。例如,User::new就是引用User类的构造方法。
Java8 ::有哪些应用场景?
由于「::」操作符可以方便地引用类的方法,并返回函数接口,因此在需要传递函数作为参数或者需要简化代码的场景中非常有用。例如,在集合操作、事件处理、替代策略模式、流式编程等场景中,都可以看到它的身影。此外,由于方法引用符可以替代lambda表达式,因此在需要减少代码冗余和提高可读性的地方,也可以考虑使用它。
80、Java 8中parallel是干嘛的?
Stream.parallel() 方法是 Java 8 中 Stream API 提供的一种并行处理方式。在处理大量数据或者耗时操作时,使用 Stream.parallel() 方法可以充分利用多核 CPU 的优势,提高程序的性能。
Stream.parallel() 方法是将串行流转化为并行流的方法。通过该方法可以将大量数据划分为多个子任务交由多个线程并行处理,最终将各个子任务的计算结果合并得到最终结果。使用 Stream.parallel() 可以简化多线程编程,减少开发难度。
需要注意的是,并行处理可能会引入线程安全等问题,需要根据具体情况进行选择。
回复面试,加入Java面试交流群
·················END·················
本系列是【10万字208道Java经典面试题总结(附答案)】的2024修订版,打造Java面试题一站式解决方案。
新建了Java面试交流群,有兴趣的小伙伴可以添加哪吒微信,备注面试交流群加入。