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
具有一定的动态性,表现在以下几个方面:
1.
反射机制;
2.
动态字节码操作;
3.
动态编译;
4.
执行其他脚本代码;
6
、
==
和
equals
的区别是什么?
(
1
)类型与定义
==
是一个操作符,用于比较两个变量的值。对于基本数据类型,它比较的是变量存储的值是否相等;对于引用类型,它比较的是两个引用是
否指向内存中的同一地址(即是否是同一个对象的引用)。
equals()
是
Object
类中的一个方法,用于比较两个对象的内容是否相等。默认情况下,它比较的是对象的内存地址(即是否是同一个对
象),但该方法可以被重写以提供自定义的比较逻辑。
(
2
)运行速度
==
:由于只是比较引用或内存地址,所以通常比
equals()
方法更快。
equals()
:由于可能需要执行更复杂的比较逻辑(尤其是在被重写的情况下),因此其运行速度可能慢于
==
。
(
3
)可重写性
==
:不可重写,其行为是固定的。
equals()
:可以被重写以提供自定义的相等性判断逻辑。
下面程序的输出结果是什么?
当
"=="
运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运
算)则比较的是数值(即会触发自动拆箱的过程)。另外,对于包装器类型,
equals
方法并不会进行类型转换。
public class
Main
{
public static
void
main
(
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
}
}
第一个和第二个输出结果没有什么疑问。第三句由于
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
x
二进制
10000001
;
y
二进制
10000000
;
根据与运算符的运算规律,只有两个位都是
1
,结果才是
1
,可以知道结果就是
10000000
,即
128
。
(
2
)或运算符
|
两个位只要有一个为
1
,那么结果就是
1
,否则就为
0
。
(
3
)非运算符
~
如果位为
0
,结果是
1
,如果位为
1
,结果是
0
。
(
4
)异或运算符
^
两个操作数的位中,相同则结果为
0
,不同则结果为
1
。
(
5
)左移运算符
<<
、右移运算符
>>
移位运算符的右操作数要完成模
32
的运算。
1<<35
的值等同于
1<<3
或
8
。
(
6
)
>>>
运算符会用
0
填充高位,不存在
<<<
运算符。
9
、说说运算符的优先级
这道题属实有点变态,这谁能记得住,运算符优先级只是一种约定,实际使用时应根据具体情况加上括号以明确运算顺序。
Java
中的运算符按照优先级从高到低依次为:
1. ()
:括号运算符,具有最高优先级;
2. !
、
~
、
++
、
--
:逻辑非、位取反、自增、自减运算符;
3. *
、
/
、
%
:乘、除、取模运算符,优先级相同,从左向右结合;
4. +
、
-
:加、减运算符,优先级相同,从左向右结合;
5. <<
、
>>
、
>>>
:左移、右移、无符号右移运算符,优先级相同,从左向右结合;
6. <
、
<=
、
>
、
>=
:小于、小于等于、大于、大于等于运算符,优先级相同,从左向右结合;
7. ==
、
!=
:等于、不等于运算符,优先级相同,从左向右结合;
8. &
:按位与运算符,优先级较低;
9. ^
:按位异或运算符,优先级更低;
package
com
.
nezha
.
javase
;
public class
Test
{
public static
void
main
(
String
[]
args
) {
int
x
=
129
;
int
y
=
128
;
System
.
out
.
println
(
"x
和
y
与的结果是:
"
+
(
x
&
y
));
// 128
}
}
package
com
.
nezha
.
javase
;
public class
Test
{
public static
void
main
(
String
[]
args
) {
int
x
=
129
;
int
y
=
128
;
System
.
out
.
println
(
"x
和
y
或的结果是:
"
+
(
x
|
y
));
// 129
}
}
public static
void
main
(
String
[]
args
) {
System
.
out
.
println
(
1
<<
35
);
//8
System
.
out
.
println
(
1
<<
3
);
//8
}
10. |
:按位或运算符,优先级最低;
11. &&
:逻辑与运算符,优先级较低;
12. ||
:逻辑或运算符,优先级更低;
13. ?:
:三元条件运算符,优先级最低;
10
、
final
在
java
中有什么作用?
(
1
)用来修饰一个引用
如果引用为基本数据类型,则该引用为常量,该值无法修改;
如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
(
2
)用来修饰一个方法
当使用
final
修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
(
3
)用来修饰类
当用
final
修改类时,该类成为最终类,无法被继承。
比如常用的
String
类就是最终类。
11
、使用
final
关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?
当使用
final
关键字修饰变量时,这意味着该变量的引用地址被固定,它不能再指向另一个对象或值。然而,这并不意味着该引用所指向的对
象的内容也不能改变。事实上,如果这个变量指向的是一个可变对象(如数组、集合或任何其他用户定义的可变类实例),那么对象的内容
是可以修改的。
12
、
this
和
super
关键字的作用
(
1
)
this
关键字的作用
1.
对象内部指代自身的引用;
2.
解决成员变量和局部变量的同名问题;
3.
可以调用成员变量,不能调用局部变量;
4.
可以调用成员方法。
(
2
)
super
关键字的作用
1.
调用父类的成员变量或方法
2.
调用父类的构造函数
13
、在
Java
中,为什么不允许从静态方法中访问非静态变量?
1.
静态变量属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问;
2.
非静态变量属于类的对象,只有在类的对象产生时,才会分配内存,通过类的实例去访问;
3.
静态方法也属于类本身,但是此时没有类的实例,内存中没有非静态变量,所以无法调用。
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.
先对各位取反;
2.
将其转换为十进制数;
3.
加上负号,再减去
1
;
例如
10000000
,最高位是
1
,是负数,①对各位取反得
01111111
,转换为十进制就是
127
,加上负号得
-127
,再减去
1
得
-128
;
下面这段代码的输出结果是什么?
也许有些朋友会说都会输出
false
,或者也有朋友会说都会输出
true
。但是事实上输出结果是:
为什么会出现这样的结果?输出结果表明
i1
和
i2
指向的是同一个对象,而
i3
和
i4
指向的是不同的对象。此时只需一看源码便知究竟,下面这段
代码是
Integer
的
valueOf
方法的具体实现:
通过
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
、什么是汇编语言?
汇编语言是一种低级计算机编程语言,它使用一种非常接近于计算机硬件的指令系统。因此,汇编语言也被认为是一种次级的计算机语言。
汇编语言的特点包括:
1.
汇编语言可以提供对计算机硬件的直接访问,因此它被用于编写操作系统和嵌入式系统等高性能的程序。
2.
汇编语言具有非常高的执行效率,因为它不需要进行高级语言的编译,也不需要进行解释,可以直接在硬件上执行。
3.
汇编语言的代码密度非常高,因为它的指令系统非常紧凑,可以有效地利用内存空间。
4.
汇编语言的执行速度非常快,因为它的指令可以直接被计算机硬件理解并执行。
5.
汇编语言需要程序员有更深入的计算机体系结构和硬件知识,因为它的指令系统比较复杂,编写难度也比较大。
总的来说,汇编语言是一种面向机器的低级语言,它直接访问计算机硬件,具有高执行效率和代码密度等优点。但因为它的指令系统复杂,
编写难度较大,需要程序员有较高的技术水平。
21
、
Java
属于什么语言?
Java
属于高级语言的一种,高级语言是一种与具体硬件和操作系统无关的编程语言,它更接近于自然语言和数学语言,具有更高的可读性和
可维护性。
高级语言的特点包括:
1.
高级语言具有更强的可读性和可维护性,因为它的语法结构和自然语言更为接近,可以更容易地被人类理解。
2.
高级语言具有更高的抽象能力,可以更容易地表达复杂的算法和逻辑结构,同时也更容易被程序员理解和维护。
3.
高级语言的指令系统通常更为复杂,需要编译器将高级语言代码转换为机器码,因此高级语言的代码通常比较大。
4.
高级语言可以提供更多的功能和特性,例如变量、函数、循环、条件语句、数组、对象等,使得程序编写更加方便和灵活。
5.
高级语言可以更好地支持面向对象编程(
OOP
)的特性,例如封装、继承、多态等,这使得程序更加模块化和可扩展。
高级语言是一种与具体硬件和操作系统无关的编程语言,它更接近于自然语言和数学语言,具有更高的可读性和可维护性。虽然它的指令系
统复杂,但因为它提供了更多的功能和特性,使得程序编写更加方便和灵活。
public class
Main
{
public static
void
main
(
String
[]
args
) {
Integer
i1
=
100
;
Integer
i2
=
100
;
Integer
i3
=
200
;
Integer
i4
=
200
;
System
.
out
.
println
(
i1
==
i2
);
System
.
out
.
println
(
i3
==
i4
);
}
}
true
false
public static
Interger
valueOf
(
int
i
){
if
(
i
>=-
128
&&
i
<=
IntergerCache
.
high
){
return
IntergerCache
.
cache
[
i
+
128
];
}
else
{
return new
Interger
(
i
);
}
}
22
、
JAVA
中有几种基本数据类型,各自占用多少字节呢?
以
int
和
Integer
为例,对比一下基本数据类型和封装数据类型。
1. int
类型,直接定义一个变量名赋值即可,是
Integer
需要使用
new
关键字创建对象;
2.
基本类型和
Integer
类型混合使用时,
Java
会自动通过拆箱和装箱实现类型转换;
3. Integer
的默认值是
null
,而
int
的默认值是
0
;
4. Integer
存储在堆内存,
int
类型是直接存储在栈空间;
5. Integer
作为一个对象类型,封装了一些方法和属性;
23
、数据类型之间的如何转换?
在
Java
中,允许进行数值转换,有些情况会丢失一部分精度。
强制类型转换的一般表示形式为:
强制类型转换通过
(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
)
getClass()
final
方法,返回
Class
类型的对象,反射来获取对象。
double
d
=
10.2
;
int
i
=
(
int
)
d
;
System
.
out
.
println
(
i
);
//10
(
3
)
toString()
toString()
方法返回一个字符串。
toString()
无处不在,只要对象与一个字符串通过操作符
+
拼接,
Java
编译器就会自动地调用
toString
方法来获得这个对象的字符串描述。
还有我们最常用的
System.out.println(name)
方法,
println
会自动的调用
name.toString()
,并打印返回的字符串。
(
4
)
finalize()
该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
(
5
)
equals()
判断内容是否相等,注意,这里比较的不是内存地址。
java
语言规范要求
equals
方法具有下面的特性:
1.
自反性:对于任何非空引用
x,x.equals(x)
应该返回
true;
2.
对称性:对于任何引用
x,
和
y,
当且仅当
,y.equals(x)
返回
true,x.equals(y)
也应该返回
true;
3.
传递性:对于任何引用
x,y,z,
如果
x.equals(y)
返回
true,y.equals(z)
返回
true,
那么
x.equals(z)
也应该返回
true;
4.
一致性:如果
x,y
引用的对象没有发生变化
,
反复调用
x.equals(y)
应该返回同样的结果
;
5.
对于任意非空引用
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)
设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生:
1.
其他线程调用了该对象的
notify
方法;
2.
其他线程调用了该对象的
notifyAll
方法;
3.
其他线程调用了
interrupt
中断该线程;
4.
时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个
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()
方法。
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
中有以下特点:
1.
它不能被继承。
2.
它不能有子类。
3.
尝试创建
final
类的子类会导致编译错误。
因此,由于
String
类是
final
的,你不能创建它的子类。在需要自定义字符串行为的情况下,你可以考虑使用其他方式,例如创建包含
String
对
象的新类,并在其中实现所需的行为。
34
、
String
,
Stringbuffer
,
StringBuilder
的区别?
从
4
个角度进行对比:
(
1
)可变性
String
内部的
value
值是
final
修饰的,所以它是不可变类。所以每次修改
String
的值,都会产生一个新的对象。
StringBuffer
和
StringBuilder
是可变类,字符串的变更不会产生新的对象。
(
2
)线程安全性
String
是不可变类,所以它是线程安全的。
StringBuffer
是线程安全的,因为它每个操作方法都加了
synchronized
同步关键字。
StringBuilder
不是线程安全的,所以在多线程环境下对字符串进行操作,应该使用
StringBuffer
,否则使用
StringBuilder
。
public class
ReverseString
{
public static
void
main
(
String
[]
args
) {
String
originalString
=
"Hello, World!"
;
StringBuilder
sb
=
new
StringBuilder
(
originalString
);
String
reversedString
=
sb
.
reverse
().
toString
();
System
.
out
.
println
(
reversedString
);
//
输出
: "!dlroW ,olleH"
}
}
(
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
个属性:
1.
容量
capacity
,缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,且永远不能被改变;
2.
读写位置
position
,下一个要被读或写的元素的索引。位置会自动由相应的
get()
和
put()
函数更新。 这里需要注意的是
positon
的位置
是从
0
开始,比如,已经写入
buffer 3
个元素那那么
position
就是指向第
4
个位置,即
position
设置为
3
(数组从
0
开始计);
3.
界限
limit
,缓冲区的第一个不能被读或写的元素。缓冲区创建时,
limit
的值等于
capacity
的值。假设
capacity = 1024
,我们在程序中
设置了
limit = 512
,说明
Buffer
的容量为
1024
,但是从
512
之后既不能读也不能写,因此可以理解成,
Buffer
的实际可用大小为
512
;
4.
可选的标记
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
进行复位。
然后可以 用某个通道的数据填充缓冲区,或者将缓冲区的内容写出到通道中。
Buffer
及其子类都不是线程安全的,若多线程操作该缓冲区,则应通过同步来控制对该缓冲区的访问。
ByteBuffer buffer
=
ByteBuffer
.
allocate
(
RECORD_SIZE
);
channel
.
read
(
buffer
);
channel
.
position
(
newpos
);
buffer
.
flip
();
channel
.
write
(
buffer
);
37
、
hashcode
是什么?有什么作用?
hashCode
主要用于获取对象的哈希码值。
这个哈希码值是一个整数,主要用于数据结构(如哈希表)中,以快速定位对象的位置。
Java
的集合框架(如
HashSet
、
HashMap
等)在内部使用了哈希表来存储元素,这些集合在添加、删除或查找元素时,都会使用到元素的
hashCode
。当我们需要查找一个对象时,哈希表会使用该对象的
hashCode
来计算对象在哈希表中的位置;
当要对对象进行比较时,可以先比较两对象的
hashcode
值
如果
hashcode
值相等,则再用
equals
进行比较;
如果
hashcode
值不等,则无需再进行
equals
比较,两对象一定不等。
38
、
Java
创建对象有几种方式
1.
使用
new
关键字,这是最常见的创建对象的方式。通过调用类的构造方法(构造器)来创建对象。
2.
使用反射,通过
Java
的反射
API
可以动态地创建对象。反射允许在运行时获取类的信息,并可以调用类的构造器来创建对象。
3.
使用克隆,如果一个类实现了
Cloneable
接口并重写了
Object
类的
clone()
方法,那么可以通过调用对象的
clone()
方法来创建该对象的一
个副本。
4.
使用序列化与反序列化,如果一个类实现了
Serializable
接口,那么可以通过序列化(将对象转换为字节流)和反序列化(将字节流转
换回对象)来创建对象。这种方式常用于对象的持久化存储和传输。
5.
使用依赖注入,在依赖注入框架(如
Spring
)中,对象的创建和管理通常由框架负责。通过配置或注解,框架会自动创建所需的对象,
并将其注入到需要的地方。
6.
使用工厂模式,工厂模式是一种创建对象的设计模式,它隐藏了对象创建的具体逻辑,并通过一个统一的接口来创建对象。工厂模式可
以分为简单工厂、工厂方法和抽象工厂等。
7.
使用构建器模式,构建器模式(
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
、普通类和抽象类有哪些区别?
1.
实例化:普通类可以被实例化,即可以创建这个类的对象。然而,抽象类不能被实例化。尝试实例化抽象类会导致编译错误。这是因为
抽象类通常表示一种概念或行为,而不是具体的对象。
2.
抽象方法:抽象类可以包含抽象方法,这是没有实现的方法,只有方法的声明,没有具体的实现。普通类则不能包含抽象方法。如果一
个类包含抽象方法,那么这个类必须被声明为抽象类。
3.
继承:一个类可以从一个抽象类继承,但也可以从普通类继承。然而,一个抽象类只能被另一个类继承,而不能被实例化。这意味着抽
象类是类的基类,用于定义一些通用的属性和方法,而具体的实现则由子类来完成。
4.
设计目的:普通类通常用于表示具体的实体或对象,如一个具体的动物或人。而抽象类则用于表示一种概念或一组具有共同特性的对
象,这些对象的具体实现由子类来完成。例如,一个
"
动物
"
类可能是一个抽象类,而
"
狗
"
和
"
猫
"
则可能是这个抽象类的具体实现。
总的来说,普通类和抽象类在实例化、包含抽象方法、继承和设计目的等方面存在明显的差异。抽象类提供了一种方式来定义一组具有共同
特性的对象的概念,而具体的实现则由子类来完成。这使得抽象类在面向对象编程中非常有用,尤其是在需要创建一组具有共同行为的对象
时。
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
)其它注意事项
1.
避免使用
Java
保留字和特殊字符作为类名、属性名或方法名。
2.
在命名时,应考虑到代码的可读性和可维护性,避免使用过于复杂或难以理解的名称。
3.
保持命名的一致性,例如在整个项目中,如果某个特定的概念或对象使用了特定的命名方式,那么在其他地方也应遵循相同的命名方
式。
49
、
Java
内部类是什么,有哪些应用场景?
Java
内部类是定义在另一个类中的类。
内部类可以访问外部类的所有成员(包括私有成员),并且可以隐藏外部类的某些成员。内部类主要有四种类型:静态内部类、成员内部
类、局部内部类和匿名内部类。
应用场景:
1.
每个内部类都可以独立的继承一个类,所以无论外部类是否继承了某个类,内部类依然可以继承其他类,这就完美的解决了
java
没有多
继承的问题。
2.
可以有效的将有一定关系的类组织在一起,又可以对外界有所隐藏。
3.
方便编写事件驱动程序
4.
方便编写多线程代码
5.
用来装逼,代码越复杂,是不是感觉越牛逼。
50
、静态内部类与非静态内部类有什么区别
1.
定义位置与访问权限:静态内部类定义在类的内部,使用
static
关键字进行定义。它只能访问外部类的静态成员变量和方法,而不能访
问外部类的非静态成员。非静态内部类可以定义在外部类的任意位置,包括方法内部(此时称为局部内部类)。非静态内部类可以直接
访问外部类的所有成员,包括私有成员。
2.
创建实例的方式:生成一个静态内部类不需要外部类成员,静态内部类的对象可以直接生成。而对于非静态内部类,其创建依赖于外部
类的实例,不能独立存在,即需要有一个外部类的实例来引用它。
3.
应用场景:静态内部类由于其不能访问外部类的非静态成员,因此在某些情况下,如仅需要访问外部类的静态成员时,使用静态内部类
会更为合适。非静态内部类则因其可以访问外部类的所有成员,包括私有成员,所以在需要这种访问权限的场景下更为常用。
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
、常见的异常类有哪些?
1. NullPointerException
:空指针异常;
2. SQLException
:数据库相关的异常;
3. IndexOutOfBoundsException
:数组下角标越界异常;
4. FileNotFoundException
:打开文件失败时抛出;
5. IOException
:当发生某种
IO
异常时抛出;
6. ClassCastException
:当试图将对象强制转换为不是实例的子类时,抛出此异常;、
7. NoSuchMethodException
:无法找到某一方法时,抛出;
8. ArrayStoreException
:试图将错误类型的对象存储到一个对象数组时抛出的异常;
9. NumberFormatException
:当试图将字符串转换成数字时,失败了,抛出;
10. IllegalArgumentException
抛出的异常表明向方法传递了一个不合法或不正确的参数。
11. ArithmeticException
当出现异常的运算条件时,抛出此异常。例如,一个整数
“
除以零
”
时,抛出此类的一个实例。
56
、开发中,你是如何处理异常的?
方法内如果抛出需要检测的异常,那么方法上必须要声明,否则必须在方法内用
try-catch
捕捉,否则编译失败。
如果调用了声明异常的函数,要么
try-catch
要么
throws
,否则编译失败。
什么时候
catch
,什么时候
throws
?功能内容可以解决,用
catch
,解决不了,用
throws
告诉调用者,有调用者解决。
如果一个功能抛出了多个异常,那么调用时必须有对应多个
catch
进行针对性的处理。
57
、
NoClassDefFoundError
和
ClassNotFoundException
有什么区别?
NoClassDefFoundError
是
JVM
运行时通过
classpath
加载类时,找不到对应的类而抛出的错误。
ClassNotFoundException
:如果在编译过程中可能出现此异常,在编译过程中必须将其抛出。
NoClassDefFoundError
的发生场景:
1.
类依赖的
class
或
jar
不存在
2.
类文件存在,但是在不同的域中,简而言之,就是找不到
ClassNotFoundException
的发生场景:
1.
调用
class
的
forName
方法时,找不到指定的类
2. ClassLoader
中的
findSystemClass()
方法时,找不到指定的类
58
、聊聊记录日志的规范
(
1
)日志的可读性,日志是记录问题的,然后让维护人员看的,规范的、通俗易懂的日志才是王道。
(
2
)日志的性能,不管是记录到文件里,还是记录到数据库里,记录日志肯定是要消耗程序性能的,这样,哪些需要记下,哪些不用记,
需要权衡利弊。
(
3
)大的循环中,尽量不要记录日志。
(
4
)日志的级别,一般情况下,程序运行时记录
info
日志,发生异常时记录
error
日志,也可以记录警告日志,比如某些参数超过了,但是
不影响整体程序的运行。
59
、哪些内容需要记在日志里?
哪些内容需要记录在日志中,日志不是越多越好,越丰满越好,凡是要有一个度。
1.
日期;
2.
时间;
3.
日志级别;
4.
代码位置;
5.
线程号;
6.
日志内容;
7.
错误码;
8.
错误信息
9.
业务描述;
10.
关键字,比如产品
id
;
11.
入参、回参;
总之一句话,你认为哪些参数比较重要,有助于你排查问题,就记录哪些。
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
、序列化使用场景有哪些?
1.
像银行卡、密码这些字段不能被序列化;
2.
将对象存储到文件中时进行序列化,从文件中读取对象时需要反序列化;
3.
分布式传递对象,或者网络传输,需要序列化;
4.
存入缓存数据库(如
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
)如何实现对象克隆?
1.
实现
Cloneable
接口,重写
clone
方法;
2.
实现
Serializable
接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆;
3. BeanUtils
,
apache
和
Spring
都提供了
bean
工具,只是这都是浅克隆。
(
3
)深拷贝和浅拷贝区别是什么?
1.
浅拷贝,指的是重新分配一块内存,创建一个新的对象,指针指向被复制对象的同一块内存地址,如果原对象发生改变,那么浅拷贝得
到的新对象也会反映出这些变化;
2.
深拷贝:它是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象
中。这意味着深拷贝不仅复制了对象本身,还复制了对象所引用的所有其他对象。因此,新对象和原对象没有任何关联,对其中一个对
象的修改不会影响另一个对象。
65
、
Java
反射是什么?有哪些应用场景
Java
反射是
Java
语言的一个核心特性,它允许程序在运行时检查类、接口、字段和方法的信息,并能动态地调用对象的方法。反射的主要作
用是增强程序的灵活性,使得程序能够在运行时动态地加载、链接和使用类。
但反射的代码比正常调用的代码更多,性能更慢,应避免使用反射。
Java
反射应用场景:
1.
在框架设计中,反射常被用于实现框架的扩展性和灵活性。框架可以通过反射在运行时加载和使用不同的类,从而实现对不同业务逻辑
的支持;
2.
通过反射,可以创建动态代理对象,这些代理对象可以在运行时代表其他对象执行操作。这在实现
AOP
(面向切面编程)等高级功能时
非常有用;
3.
反射可以解析注解信息,并执行相应的操作;
4.
在如
Hibernate
这样的
ORM
框架中,对象需要被转化为实体类并存储到数据库中。这个过程中,反射被用于动态创建实体类对象、获取
类的属性和方法等;
5.
在
Java
中,序列化和反序列化都需要使用到反射技术。序列化会将对象转化成字节流,反序列化则将字节流还原为对象。在这个过程
中,需要借助反射技术来获取对象的属性信息。
Java
反射提供了一种强大的机制,使得程序能够在运行时动态地操作类和对象,从而增强了程序的灵活性和可扩展性。然而,反射也有一些
潜在的性能开销和安全性问题,因此在使用时需要谨慎考虑。
66
、
java
中都有哪些引用类型?
(
1
)强引用
Java
中默认声明的就是强引用,比如:
只要强引用存在,垃圾回收器将永远不会回收被引用的对象。如果想被回收,可以将对象置为
null
;
(
2
)软引用(
SoftReference
)
在内存足够的时候,软引用不会被回收,只有在内存不足时,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,
才会跑出内存溢出异常。
(
3
)弱引用(
WeakReference
)
进行垃圾回收时,弱引用就会被回收。
(
4
)虚引用(
PhantomReference
)
(
5
)引用队列(
ReferenceQueue
)
引用队列可以与软引用、弱引用、虚引用一起配合使用。
当垃圾回收器准备回收一个对象时,如果发现它还有引用,就会在回收对象之前,把这个引用加入到引用队列中。
程序可以通过判断引用队列中是否加入了引用,来判断被引用的对象是否将要被垃圾回收,这样可以在对象被回收之前采取一些必要的措
施。
67
、枚举是什么,有哪些特点?
1.
枚举作为一个类
,
可以有自己的属性
(
通常应该是常量,我没遇到过不是的情况
)
以及自己的方法
(
否则只能用
switch
来写,实际违反原则
)
2.
枚举类型检查,有效性检查
3.
和常量相比,无需查看文档和源码就能直接知道所有可能返回值,方便编码。
4.
与
switch
配合使用解决
ifelse
过多的问题,使用
switch
进行条件判断时,条件参数一般只能是整型,字符型。而枚举型确实也被
switch
所支持,在
java 1.7
后
switch
也对字符串进行了支持。
68
、泛型是什么,有哪些应用场景?
泛型是一种编程范式,其核心思想是将类型参数化,即把数据类型作为参数传递。通过泛型,可以创建灵活且可重用的代码,而无需针对每
种数据类型都编写重复的代码。泛型的主要优点包括提高代码的可读性、可维护性和类型安全性。
应用场景:
1.
集合类和数据结构:泛型最常见的用途是在集合类(如
ArrayList
、
LinkedList
、
HashMap
等)和数据结构中使用。通过使用泛型,可
以创建一个通用的集合类,用于存储不同类型的元素,并在编译时捕获类型错误。
2.
自定义数据结构:使用泛型可以创建自定义的数据结构,以适应不同类型的数据。这样可以编写通用的、可重用的代码,减少为不同类
型的数据编写不同实现的需求。
3.
泛型方法:除了泛型类,还有泛型方法,即在方法级别使用泛型。这对于那些只需要在特定方法中使用泛型的情况非常有用。
4.
接口和抽象类:泛型也可以用于接口和抽象类的定义,以创建通用的接口和抽象类,这些接口和类可以被不同类型的实现或子类使用。
5.
数据库操作:在数据库操作中,泛型可以用于定义数据库表中各个字段的类型,从而提高程序的类型安全性。
使用泛型的主要目的之一是提供编译时类型检查,这有助于减少在运行时出现类型错误的可能性。此外,泛型还可以提高代码的可读性和重
用性,避免不必要的类型转换和强制类型转换,从而提高代码的安全性和可维护性。
Object
obj
=
new
Object
();
obj
=
null
;
byte
[]
buff
=
new
byte
[
1024
*
1024
];
SoftReference
<
byte
[]
>
sr
=
new
SoftReference
<>
(
buff
);
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
有哪些应用场景
1. BIO
方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,
JDK1.4
以前的唯一选择,但程
序简单易理解。
2. NIO
方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕 系统,服务器间通讯等。编程比较复杂,
JDK1.4
开
始支持。
3. AIO
方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分 调用
OS
参与并发操作,编程比较复杂,
JDK7
开始
支持
72
、简述一下
BIO
的编程流程
1.
服务器端启动一个
ServerSocket
;
2.
客户端启动
Socket
对服务器进行通 信,默认情况下服务器端需要对每 个客户 建立一个线程与之通讯;
3.
客户端发出请求后
,
先咨询服务器 是否有线程响应,如果没有则会等 待,或者被拒绝;
4.
如果有响应,客户端线程会等待请 求结束后,在继续执行;
73
、
NIO
的三大核心部分是什么?
Selector(
选择器
)
、
Channel(
通道
)
、
Buffer(
缓冲区
)
。
1. Selector
对应一个线程, 一个线程对应多个
channel(
连接
)
;
2.
该图反应了有三个
channel
注册到 该
selector //
程序;
3.
每个
channel
都会对应一个
Buffer
;
4.
程序切换到哪个
channel
是有事件决定的
, Event
就是一个重要的概念;
5. Selector
会根据不同的事件,在各个通道上切换;
6. Buffer
就是一个内存块 , 底层是有一个数组;
7.
数据的读取写入是通过
Buffer,
这个和
BIO , BIO
中要么是输入流,或者是 输出流
,
不能双向,但是
NIO
的
Buffer
是可以读也可以写
,
需要
flip
方法切换;
8. channel
是双向的
,
可以返回底层操作系统的情况
,
比如
Linux
, 底层的操作系统 通道就是双向的;
NIO
是面向缓冲区,或者说面向块编程,数据读取到一个 它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就 增加了处理过程中的灵
活性,使用它可以提供非阻塞式的高伸缩性网络。
HTTP2.0
使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求 的数量比
HTTP1.1
大了好几个数量级。
缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个 容器对象
(
含数组
)
,该对象提供了一组方法,可以更轻松地使用内存块,,
缓冲区对 象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel
提供从文件、 网络读取数据的渠道,但是读取或写入的数
据都必须经由
Buffer
。
74
、
NIO
中
buffer
的四大属性是什么?
1. mark
:标记
2. position
:位置,下一个要被读或写的元素的索引, 每次读写缓冲区数据时都会改变改值, 为下次读写作准备。
3. limit
:表示缓冲区的当前终点,不能对缓冲区 超过极限的位置进行读写操作。且极限 是可以修改的
4. capacity
:容量,即可以容纳的最大数据量;在缓 冲区创建时被设定并且不能改变。
75
、对比一下
BIO
和
NIO
?
1. BIO
以流的方式处理数据
,
而
NIO
以块的方式处理数据
,
块
I/O
的效率比流
I/O
高很多;
2. BIO
是阻塞的,
NIO
则是非阻塞的;
3. BIO
基于字节流和字符流进行操作,而
NIO
基于
Channel(
通道
)
和
Buffer(
缓冲区
)
进 行操作,数据总是从通道读取到缓冲区中,或者从
缓冲区写入到通道中。
Selector(
选择器
)
用于监听多个通道的事件(比如:连接请求,数据到达等),因 此使用单个线程就可以监听多
个客户端通道。
76
、
FileChannel
是做什么的?
FileChannel
主要用来对本地文件进行
IO
操作,常见的方法有:
1. read
,从通道读取数据并放到缓冲区中
2. write
,把缓冲区的数据写到通道中
3. transferFrom
,从目标通道 中复制数据到当前通道
4. transferTo
,把数据从当 前通道复制给目标通道
77
、简述一下
Selector
选择器
1. Java
的
NIO
,用非阻塞的
IO
方式。可以用一个线程,处理多个的客户端连 接,就会使用到
Selector(
选择器
)
。
2. Selector
能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然 后针对每个事件进行相应的处理。这样就可以
只用一个单线程去管理多个 通道,也就是管理多个连接和请求。
3.
只有在 连接
/
通道 真正有读写事件发生时,才会进行读写,就大大地减少 了系统开销,并且不必为每个连接都创建一个线程,不用去维
护多个线程。
4.
避免了多线程之间的上下文切换导致的开销。
78
、
lambda
表达式是什么,有哪些应用场景?
在
Java 8
中,
Lambda
表达式被引入作为一个重要新特性,使得
Java
能够进行函数式编程,并在并发性能上取得了实质性的进步。
Lambda
表
达式简化了匿名内部类的形式,并达到了同样的效果,使得代码更加简洁。
应用场景:
1.
集合操作:
Lambda
表达式可以与集合操作方法(如
forEach
、
filter
、
map
、
reduce
)结合使用,对集合中的元素进行遍历、筛选、映
射、聚合等操作。
2.
排序:
Lambda
表达式可以用于自定义的排序功能,通过传递不同的比较规则实现对集合中元素的排序。
3.
线程编程:
Lambda
表达式可以简化线程编程中的代码,例如使用
Lambda
表达式创建
Runnable
对象,或使用
Lambda
表达式实现函数
式接口来处理线程任务。
4. GUI
事件处理:
Lambda
表达式可用于简化
GUI
事件处理代码,如为按钮、菜单等组件注册事件监听器。
5.
数据处理:对于大数据集的处理,如统计、过滤、转换等,
Lambda
表达式表现出色。其并行处理的能力可以提高数据处理的效率。
6. Web
开发:
Lambda
表达式可以简化
Web
开发中的重复性代码,例如通过
Lambda
表达式实现控制器、过滤器、拦截器等。
79
、
Java8 ::
是什么,有哪些应用场景?
在
Java 8
中,双冒号
“::”
是一个方法引用操作符,也被称为方法引用符。它用于引用类的方法,并返回一个函数接口(
function interface
)。
这与
lambda
表达式有所不同,因为
lambda
表达式需要自定义一个
lambda
体,而
“::”
则直接引用一个已存在的方法。
“::”
操作符在
Java 8
中有多种用法:
1.
引用静态方法:使用
“
类名
::
静态方法名
”
的格式。例如,
Integer::parseInt
就是引用
Integer
类的
parseInt
静态方法。
2.
引用对象方法:使用
“
实例对象
::
实例方法
”
的格式。例如,对于字符串的
substring
方法,可以写成
String::substring
。
3.
引用构造方法:使用
“
类名
::new”
的格式。例如,
User::new
就是引用
User
类的构造方法。
Java8 ::
有哪些应用场景?
由于
“::”
操作符可以方便地引用类的方法,并返回函数接口,因此在需要传递函数作为参数或者需要简化代码的场景中非常有用。例如,在集
合操作、事件处理、替代策略模式、流式编程等场景中,都可以看到它的身影。此外,由于方法引用符可以替代
lambda
表达式,因此在需要
减少代码冗余和提高可读性的地方,也可以考虑使用它。
80
、
Java 8
中
parallel
是干嘛的?
Stream.parallel()
方法是
Java 8
中
Stream API
提供的一种并行处理方式。在处理大量数据或者耗时操作时,使用
Stream.parallel()
方法可
以充分利用多核
CPU
的优势,提高程序的性能。
Stream.parallel()
方法是将串行流转化为并行流的方法。通过该方法可以将大量数据划分为多个子任务交由多个线程并行处理,最终将各个
子任务的计算结果合并得到最终结果。使用
Stream.parallel()
可以简化多线程编程,减少开发难度。
需要注意的是,并行处理可能会引入线程安全等问题,需要根据具体情况进行选择。