Java基础总结:
基础概念与常识:
Java 语言有哪些特点?
- 简单易学(语法简单,上手容易);
- 面向对象(封装,继承,多态);
- 平台无关性( Java 虚拟机实现平台无关性);
- 可靠性(具备异常处理和自动内存管理机制);
- 安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源);
- 高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的);
- 支持网络编程并且很方便;
- 编译与解释并存;
JavaSE vs JavaEE
Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。
JVM vs JDK vs JRE
- JVM(java虚拟机)是运行Java字节码的虚拟机。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
- JDK(Java Development Kit)是功能齐全的Java SDK。包括 JRE + java开发工具包(javac、javap、jconsole、javadoc等)
- JRE是Java运行时环境。 包括Java虚拟机(JVM)、Java基础类库(Class Library)。
什么是字节码?字节码的好处是什么?
JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。
.class->机器码
这一步。JVM类加载器首先字节码文件,然后通过解释器逐行解释执行。但有些方法和代码块是需要经常调用的(所谓热点代码),因此后面引入JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。
JDK、JRE、JVM、JIT 这四者的关系如下图所示。
为什么说Java语言“编译与解释并存”?
编译型:编译型语言 会通过编译器 将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
解释型:解释型语言 会通过解释器 一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
答:因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
Java和C++的区别?
- Java不提供指针来直接访问内存,程序内存更加安全;
- Java的类是单继承的(接口可以多继承),C++支持多重继承;
- Java有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
- Java仅支持方法重载,但是C++同时还支持操作符重载
基本语法:
标识符和关键字的区别是什么?
- 编写程序时,需要大量地为程序、类、变量、方法等取名字,于是有了标识符。
- 关键字是被赋予特殊含义的标识符。
Java关键字有哪些?
Tips:
default
这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制。
- 在程序控制中,当在
switch
中匹配不到任何情况时,可以使用default
来编写默认匹配的情况。- 在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用
default
关键字来定义一个方法的默认实现。- 在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符
default
,但是这个修饰符加上了就会报错。
⚠️ 注意:虽然 true
, false
, 和 null
看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用。
位移运算符
<<
:左移运算符,向左移若干位,高位丢弃,低位补零。x << 1
,相当于 x 乘以 2(不溢出的情况下)。>>
:带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1
,相当于 x 除以 2。>>>
:无符号右移,忽略符号位,空位都以 0 补齐。
由于
double
,float
在二进制中的表现比较特殊,因此不能来进行移位操作。移位操作符实际上支持的类型只有
int
和long
,编译器在对short
、byte
、char
类型进行移位前,都会将其转换为int
类型再操作。
continue、break和return的区别是什么?
continue
:指跳出当前的这一次循环,继续下一次循环。break
:指跳出整个循环体,继续执行循环下面的语句。return
:用于结束所在方法的运行。
基本数据类型:
java中的几种基本数据类型了解吗?
- 6 种数字类型:
- 4 种整数型:
byte
、short
、int
、long
- 2 种浮点型:
float
、double
- 4 种整数型:
- 1 种字符类型:
char
- 1 种布尔型:
boolean
这八种基本类型都有对应的包装类分别为:Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
。
基本数据类型和包装数据类型的区别?
- 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
- 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。 - 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
- 默认值:成员变量包装类型不赋值就是
null
,而基本类型有默认值且不是null
。 - 比较方式:对于基本数据类型来说,
==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用equals()
方法。
⚠️ 注意:基本数据类型存放在栈中是一个常见的误区! 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中。
public class Test {
// 成员变量,存放在堆中
int a = 10;
// 被 static 修饰,也存放在堆中,但属于类,不属于对象
// JDK1.7 静态变量从永久代移动了 Java 堆中
static int b = 20;
public void method() {
// 局部变量,存放在栈中
int c = 30;
static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量
}
}
包装类型的缓存机制了解么?
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据Character
创建了数值在 [0,127] 范围的缓存数据Boolean
直接返回True
orFalse
。
⚠️ 记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
自动装箱与拆箱了解吗?原理是什么?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; //装箱
int n = i; //拆箱
装箱其实就是调用了 包装类的valueOf()
方法,拆箱其实就是调用了 xxxValue()
方法。
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
为什么浮点数运算的时候会有精度丢失的风险?
示例:
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
注意:计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
比如说十进制下的 0.2 就没办法精确转换成二进制小数:
// 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
可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
注意:BigDecimal
具体使用内容参考BigDecimal 详解 | JavaGuide
超过 long 整型的数据应该如何表示?
在 Java 中,64 位 long 整型是最大的整数类型。
long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true
BigInteger
内部使用 int[]
数组来存储任意大小的整形数据。
相对于常规整数类型的运算来说,BigInteger
运算的效率会相对较低。
变量
❗成员变量与局部变量的区别?
- 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰。 - 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 - 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
为什么成员变量有默认值?
- 先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。
- 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。
- 对于编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值。
成员变量与局部变量代码示例:
public class VariableExample {
// 成员变量
private String name;
private int age;
// 方法中的局部变量
public void method() {
int num1 = 10; // 栈中分配的局部变量
String str = "Hello, world!"; // 栈中分配的局部变量
System.out.println(num1);
System.out.println(str);
}
// 带参数的方法中的局部变量
public void method2(int num2) {
int sum = num2 + 10; // 栈中分配的局部变量
System.out.println(sum);
}
// 构造方法中的局部变量
public VariableExample(String name, int age) {
this.name = name; // 对成员变量进行赋值
this.age = age; // 对成员变量进行赋值
int num3 = 20; // 栈中分配的局部变量
String str2 = "Hello, " + this.name + "!"; // 栈中分配的局部变量
System.out.println(num3);
System.out.println(str2);
}
}
静态变量有什么作用?
静态变量也就是被 static
关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
静态变量是通过类名来访问的,例如StaticVariableExample.staticVar
(如果被 private
关键字修饰就无法这样访问了)。
public class StaticVariableExample {
// 静态变量
public static int staticVar = 0;
}
通常情况下,静态变量会被 final
关键字修饰成为常量。
public class ConstantVariableExample {
// 常量
public static final int constantVar = 0;
}
字符型常量和字符串常量的区别?
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
- 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。
⚠️ 注意 char
在 Java 中占两个字节。
示例:
public class StringExample {
// 字符型常量
public static final char LETTER_A = 'A';
// 字符串常量
public static final String GREETING_MESSAGE = "Hello, world!";
public static void main(String[] args) {
System.out.println("字符型常量占用的字节数为:"+Character.BYTES);
System.out.println("字符串常量占用的字节数为:"+GREETING_MESSAGE.getBytes().length);
}
}
-------------------------------
字符型常量占用的字节数为:2
字符串常量占用的字节数为:13
方法
什么是方法的返回值?方法有哪几种类型?
- 无参数无返回值的方法
- 无参数无返回值的方法
- 有返回值无参数的方法
- 有返回值有参数的方法
静态方法为什么不能调用非静态成员?
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
静态方法和实例方法有何不同?
1、调用方式
在外部调用静态方法时,可以使用 类名.方法名
的方式,也可以使用 对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
**注意:**建议使用 类名.方法名
的方式来调用静态方法。
public class Person {
public void method() {
//......
}
public static void staicMethod(){
//......
}
public static void main(String[] args) {
Person person = new Person();
// 调用实例方法
person.method();
// 调用静态方法
Person.staicMethod()
}
}
2、访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
重载和重写有什么区别?
- 重载:发生在同一个类中,方法名相同,参数类型不同、个数不同、顺序不同等
- 重写:前提是子类继承父类。重写就是子类希望对父类的方法重新改造。
**注意:**构造方法无法重写;如果父类方法访问修饰符为private/final/static
则子类就不能重写该方法
面向对象基础:
面向对象和面向过程的区别?
- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。【面向对象开发的程序一般更易维护、易服用、易拓展】
下面是一个求圆的面积和周长的示例。
面向对象:
public class Circle { // 定义圆的半径 private double radius; // 构造函数 public Circle(double radius) { this.radius = radius; } // 计算圆的面积 public double getArea() { return Math.PI * radius * radius; } // 计算圆的周长 public double getPerimeter() { return 2 * Math.PI * radius; } public static void main(String[] args) { // 创建一个半径为3的圆 Circle circle = new Circle(3.0); // 输出圆的面积和周长 System.out.println("圆的面积为:" + circle.getArea()); System.out.println("圆的周长为:" + circle.getPerimeter()); } }
面向过程:
public class Main { public static void main(String[] args) { // 定义圆的半径 double radius = 3.0; // 计算圆的面积和周长 double area = Math.PI * radius * radius; double perimeter = 2 * Math.PI * radius; // 输出圆的面积和周长 System.out.println("圆的面积为:" + area); System.out.println("圆的周长为:" + perimeter); } }
面向对象比面向过程性能差?
第一想法,类调用需要实例化,开销比较大,比较耗资源。但面向过程也需要分配内存,计算内存偏移量。
**根本原因:**Java是半编译语言,最终执行的代码并不是可以直接被CPU执行的二进制机械码。
创建一个对象用什么运算符?对象实体与对象引用有何不同?
- new关键字来创建对象实例(对象实例在堆内存中);
- 对象引用指向对象实例(对象引用存放在栈内存中);
不同之处:
- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
对象的相等和引用相等的区别?
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
示例:
String str1 = "hello";
String str2 = new String("hello");
String str3 = "hello";
// 使用 == 比较字符串的引用相等
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // true
// 使用 equals 方法比较字符串的相等
System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // true
结论:
str1
和str2
不相等,而str1
和str3
相等。这是因为==
运算符比较的是字符串的引用是否相等。str1
、str2
、str3
三者的内容都相等。这是因为equals
方法比较的是字符串的内容,即使这些字符串的对象引用不同,只要它们的内容相等,就认为它们是相等的。
如果一个类没有声明构造方法,该程序能正确执行吗?
可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。【构造方法是我们创建对象要加()的原因】
构造方法有哪些特点?是否可被 override?
构造方法特点:
- 名字和类名相同
- 没有返回值,但不能用void声明构造函数
- 生成类的对象时自动执行,无需调用
构造方法不能被重写,但是可以重载。
面向对象三大特征
封装:
是指把对象的状态(属性)隐藏在对象内部,不允许外部对象直接访问对象的内存信息,但是会提供一些被外界访问的方法来操作属性。
public class Student {
private int id;//id属性私有化
private String name;//name属性私有化
//获取id的方法
public int getId() {
return id;
}
//设置id的方法
public void setId(int id) {
this.id = id;
}
//获取name的方法
public String getName() {
return name;
}
//设置name的方法
public void setName(String name) {
this.name = name;
}
}
继承:
不同类型的对象,相互之间会有一定的共同点。可以把这些共同点定义在父类中。子类继承父类就可以拥有父类的功能,同时可以增加子类中的功能。
**优点:**可以提高代码复用性;提高程序维护性;使得类和类产生关系,是多态的前提(也是继承的一个弊端,类的耦合性提高)
注意:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。(子类重写父类方法)
多态:
有两种体现形式:方法多态、对象多态
-
方法多态
- 重载根据传参数不一样体现多态
- 重写根据对象不同,但调用方法一样体现多态。
public class PloyMethod { public static void main(String[] args) { //方法重载体现多态 A a = new A(); //这里我们传入不同的参数,就会调用不同sum方法,就体现多态 System.out.println(a.sum(10, 20)); System.out.println(a.sum(10, 20, 30)); //方法重写体现多态 B b = new B(); a.say(); b.say(); } } class B { //父类 public void say() { System.out.println("B say() 方法被调用..."); } } class A extends B {//子类 public int sum(int n1, int n2){//和下面sum 构成重载 return n1 + n2; } public int sum(int n1, int n2, int n3){ return n1 + n2 + n3; } public void say() { System.out.println("A say() 方法被调用..."); } }
-
对象多态:一个对象的编译类型和运行类型可以不一样
-
向上转型:父类的引用指向了子类的对象
特点:
编译类型看左边,运行类型看右边
可以调用父类中有权限的所有成员;不能调用子类中特有的成员
-
向下转型:父类的引用 转为 子类类型
特点:
只能强转父类的引用,不能强转父类的对象
父类引用只能强转为目标类型的对象(比如
Animal a = new Cat()
;只能强转为Cat cat = (Cat) a
) 向下转型后可以调用子类类型中的所有成员
-
接口和抽象类有什么共同点和区别?
共同点:
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用
default
关键字在接口中定义默认方法)。
区别:
- 接口主要对类的行为进行约束,实现了某个接口就具有了对应的行为;抽象类主要用于代码复用,强调所属关系。
- 类是单继承;但是可以实现多个接口
- 接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值;而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
补充: 引用拷贝就是两个不同的引用指向同一个对象
举例:
浅拷贝
浅拷贝的示例代码如下,我们这里实现了
Cloneable
接口,并重写了clone()
方法。
clone()
方法的实现很简单,直接调用的是父类Object
的clone()
方法。public class Address implements Cloneable{ private String name; // 省略构造函数、Getter&Setter方法 @Override public Address clone() { try { return (Address) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); } } } public class Person implements Cloneable { private Address address; // 省略构造函数、Getter&Setter方法 @Override public Person clone() { try { Person person = (Person) super.clone(); return person; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } } ----------测试--------------- Person person1 = new Person(new Address("武汉")); Person person1Copy = person1.clone(); // true System.out.println(person1.getAddress() == person1Copy.getAddress());
person1
的克隆对象和person1
使用的仍然是同一个Address
对象。深拷贝:
这里我们简单对
Person
类的clone()
方法进行修改,连带着要把Person
对象内部的Address
对象一起复制。@Override public Person clone() { try { Person person = (Person) super.clone(); person.setAddress(person.getAddress().clone()); return person; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } ----------测试---------- Person person1 = new Person(new Address("武汉")); Person person1Copy = person1.clone(); // false System.out.println(person1.getAddress() == person1Copy.getAddress());
person1
的克隆对象和person1
包含的Address
对象已经是不同的了。
Object:
Object类的常见方法有哪些?
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
== 和 equals() 的区别?
1、==
对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
2、equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()
方法存在于Object
类中,而Object
类是所有类的直接或间接父类,因此所有的类都有equals()
方法。
equals()
方法存在两种使用情况:
- 类没有重写
equals()
方法:通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。 - 类重写了
equals()
方法:一般我们都重写equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
hashCode() 有什么用?
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()
定义在 JDK 的 Object
类中,这就意味着 Java 中的任何类都包含有 hashCode()
函数。另外需要注意的是:Object
的 hashCode()
方法是本地方法,也就是用 C 语言或 C++ 实现的。
为什么要有 hashCode?
因为在HashSet中,比较两个对象是否相等,一般是先比较hashCode是否一样。
- 如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。 - 如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
为什么重写 equals() 时必须重写 hashCode() 方法?
如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
String
String、StringBuffer、StringBuilder的区别?
1、可变性:
-
String是不可变的:
-
保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。 -
String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
-
-
StringBuilder
与StringBuffer
都继承自AbstractStringBuilder
类,在AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用final
和private
关键字修饰,最关键的是这个AbstractStringBuilder
类还提供了很多修改字符串的方法比如append
方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
2、线程安全性:
String
中的对象是不可变的,也就可以理解为常量,线程安全。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
3、性能:
- 每次对
String
类型进行改变的时候,都会生成一个新的String
对象,然后将指针指向新的String
对象。 StringBuffer
每次都会对StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StringBuilder
相比使用StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
4、对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String 为什么是不可变的?
- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的的方法。 String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
字符串拼接用“+” 还是 StringBuilder?
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
上面的代码对应的字节码为:
**总结:**字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
注意:在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷---------->编译器不会创建单个 StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);
StringBuilder
对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder
对象。
**注意:**Java9中解决了 使用 “+” 进行字符串拼接会产生大量的临时对象的问题
String#equals() 和 Object#equals() 有何区别?
String
中的equals
方法是被重写过的,比较的是 String 字符串的值是否相等。Object
的equals
方法是比较的对象的内存地址。
字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
String s1 = new String(“abc”);这句话创建了几个字符串对象?
答:创建1个或两个字符串对象
-
如果字符串常量池中不存在字符串对象“abc”的引用,会创建两个。
一个是堆空间中new出来的;另一个保存在字符串常量池中。【注意jdk7以后,字符串常量池在堆中】
-
如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
String#intern 方法有什么作用?
String.intern()
是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
示例代码(JDK 1.8) :
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
String 类型的变量和常量做“+”运算时发生了什么?
先来看字符串不加 final
关键字拼接的情况(JDK1.8):
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
在编译过程中,javac编译器会进行常量折叠的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中。
对于 String str3 = "str" + "ing";
编译器会给你优化成 String str3 = "string";
⚠:只有编译器在程序编译期可以确定地常量才会进行常量折叠:
基本数据类型(
byte
、boolean
、short
、char
、int
、float
、long
、double
)以及字符串常量。
final
修饰的基本数据类型和字符串变量字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
String str4 = new StringBuilder().append(str1).append(str2).toString();
异常
Java 异常类层次结构图概览:
Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。Error
:Error
属于程序无法处理的错误 ,不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译。
除了RuntimeException
及其子类以外,其他的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
对象封装的异常信息
try-catch-finally 如何使用?
try
块:用于捕获异常。其后可接零个或多个 catch
块,如果没有 catch
块,则必须跟一个 finally
块。
catch
块:用于处理 try 捕获到的异常。
finally
块:无论是否捕获或处理异常,finally
块里的语句都会被执行。当在 try
块或 catch
块中遇到 return
语句时,finally
语句块将在方法返回之前被执行。
⚠注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
示例:
public static void main(String[] args) { System.out.println(f(2)); } public static int f(int value) { try { return value * value; } finally { if (value == 2) { return 0; } } } ---------------输出-------------- 0
finally 中的代码一定会执行吗?
不一定的!在某些情况下,finally 中的代码不会被执行。
就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
try {
System.out.println("Try to do something");
throw new RuntimeException("RuntimeException");
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
// 终止当前正在运行的Java虚拟机
System.exit(1);
} finally {
System.out.println("Finally");
}
-----------------输出-----------------
Try to do something
Catch Exception -> RuntimeException
注意:以下 2 种特殊情况下,finally
块的代码也不会被执行:
- 程序所在的线程死亡。
- 关闭 CPU。
如何使用 try-with-resources
代替try-catch-finally
?
- 适用范围(资源的定义): 任何实现
java.lang.AutoCloseable
或者java.io.Closeable
的对象 - 关闭资源和 finally 块的执行顺序: 在
try-with-resources
语句中,任何 catch 或 finally 块在声明的资源关闭后运行
Java 中类似于InputStream
、OutputStream
、Scanner
、PrintWriter
等的资源都需要我们调用close()
方法来手动关闭,一般情况下我们都是通过try-catch-finally
语句来实现这个需求,如下
//读取文本文件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
使用 Java 7 之后的 try-with-resources
语句改造上面的代码:
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
通过使用分号分隔,可以在try-with-resources
块中声明多个资源。
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出
NumberFormatException
而不是其父类IllegalArgumentException
。 - 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。
- ……
泛型
什么是泛型?有什么作用?
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList
对象只能传入 Person
对象,如果传入其他类型的对象就会报错。
ArrayList<E> extends AbstractList<E>
泛型的使用方式有哪几种?
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
1.泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2.泛型接口:
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
3.泛型方法:
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
注意:
public static < E > void printArray( E[] inputArray )
一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的<E>
项目中哪里用到了泛型?
- 自定义接口通用返回结果
CommonResult<T>
通过参数T
可根据具体的返回类型动态指定结果的数据类型 - 定义
Excel
处理类ExcelUtil<T>
用于动态指定Excel
导出的数据类型 - 构建集合工具类(参考
Collections
中的sort
,binarySearch
方法)。 - ……
反射
什么是反射?
反射赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射的优缺点?
反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,例如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
反射的应用场景?
Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method
来调用指定的方法。
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
注解
什么是注解?
Annotation
(注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了Annotation
的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
JDK 提供了很多内置的注解(比如 @Override
、@Deprecated
),同时,我们还可以自定义注解。
注解的解析方法有哪几种?
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value
、@Component
)都是通过反射来进行处理的。
SPI
关于 SPI 的详细解读,请看这篇文章Java SPI 机制详解 | JavaGuide
何谓 SPI?
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
SPI 和 API 有什么区别?
SPI 的优缺点?
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个
ServiceLoader
同时load
时,会有并发问题。
序列化和反序列化
什么是序列化?什么是反序列化?
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
如果有些字段不想进行序列化怎么办?
使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
I/O
详情参考这几篇文章:
Java IO 流了解吗?
IO 即 Input/Output
,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
I/O 流为什么要分为字节流和字符流呢?
本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
主要有两点原因:
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时;
- 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。
Java IO 中的设计模式有哪些?
BIO、NIO 和 AIO 的区别?
语法糖
什么是语法糖?
语法糖(Syntactic sugar) 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
举个例子,Java 中的 for-each
就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。
String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"};
for (String s : strs) {
System.out.println(s);
}
不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。如果你去看com.sun.tools.javac.main.JavaCompiler
的源码,你会发现在compile()
中有一个步骤就是调用desugar()
,这个方法就是负责解语法糖的实现的。
Java 中有哪些常见的语法糖?
Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。
关于这些语法糖的详细解读,请看这篇文章Java 语法糖详解 | JavaGuide
过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
I/O 流为什么要分为字节流和字符流呢?
本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
主要有两点原因:
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时;
- 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。
Java IO 中的设计模式有哪些?
BIO、NIO 和 AIO 的区别?
语法糖
什么是语法糖?
语法糖(Syntactic sugar) 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
举个例子,Java 中的 for-each
就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。
String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"};
for (String s : strs) {
System.out.println(s);
}
不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。如果你去看com.sun.tools.javac.main.JavaCompiler
的源码,你会发现在compile()
中有一个步骤就是调用desugar()
,这个方法就是负责解语法糖的实现的。
Java 中有哪些常见的语法糖?
Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。
关于这些语法糖的详细解读,请看这篇文章Java 语法糖详解 | JavaGuide
参考来源: JavaGuide