Java语言的特点
简单易学(语法简单,上手容易);
面向对象(封装,继承,多态);
平台无关性( Java 虚拟机实现平台无关性);
支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
可靠性(具备异常处理和自动内存管理机制);
安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源);
高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的);
支持网络编程并且很方便;
编译与解释并存;
封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。
继承
不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
关于继承如下 3 点请记住:
-
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
-
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
-
子类可以用自己的方式实现父类的方法。
多态
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
多态的特点:
-
对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
-
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
-
多态不能调用“只在子类存在但在父类不存在”的方法;
-
如果子类重写了父类的方法,真正执行的是子类重写的方法,如果子类没有重写父类的方法,执行的是父类的方法。
JVM是什么
是运行Java字节码的虚拟机,可以运行在不同的平台上。不同编程语言(Java、Groovy、Kotlin、JRuby、Clojure ...)通过各自的编译器编译成 .class
文件,并最终通过 JVM 在不同平台(Windows、Mac、Linux)。所有编程语言都能编译成字节码。
为什么Java编译与解释共存
-
编译型:编译型语言open in new window 会通过编译器open in new window将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
-
解释型:解释型语言open in new window会通过解释器open in new window一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
标识符和关键字的区别是什么
标识符就是一个名字 。关键字是被赋予特殊含义的标识符 。
自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
-
装箱:将基本类型用它们对应的引用类型包装起来;
-
拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; //装箱 int n = i; //拆箱
浮点数运算精度丢失代码演示:
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
为什么会出现这个问题呢?
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
如何解决浮点数运算的精度丢失问题?
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的。
BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("1.00"); BigDecimal c = new BigDecimal("0.8"); BigDecimal x = a.subtract(c); BigDecimal y = b.subtract(c); System.out.println(x); /* 0.2 */ System.out.println(y); /* 0.20 */ // 比较内容,不是比较值 System.out.println(Objects.equals(x, y)); /* false */ // 比较值相等用相等compareTo,相等返回0 System.out.println(0 == x.compareTo(y)); /* true */
静态变量有什么作用?
静态变量也就是被 static
关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
静态变量是通过类名来访问的,例如StaticVariableExample.staticVar
(如果被 private
关键字修饰就无法这样访问了)。
public class StaticVariableExample { // 静态变量 public static int staticVar = 0; }
通常情况下,静态变量会被 final
关键字修饰成为常量。
字符型常量和字符串常量的区别?
-
形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 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
字符型常量和字符串常量的区别?
-
形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 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
什么是可变长参数?
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。
public static void method1(String... args) { //...... }
另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
public static void method2(String arg1, String... args) { //...... }
对象的相等和引用相等的区别
-
对象的相等一般比较的是内存中存放的内容是否相等。
-
引用相等一般比较的是他们指向的内存地址是否相等。
这里举一个例子:
String str1 = "hello"; String str2 = new String("hello"); String str3 = "hello"; // 使用 == 比较字符串的引用相等 System.out.println(str1 == str2); System.out.println(str1 == str3); //字符串在常量池里,所以地址相同 // 使用 equals 方法比较字符串的相等 System.out.println(str1.equals(str2)); System.out.println(str1.equals(str3));
输出结果:
false true true true
从上面的代码输出结果可以看出:
-
str1
和str2
不相等,而str1
和str3
相等。这是因为==
运算符比较的是字符串的引用是否相等。 -
str1
、str2
、str3
三者的内容都相等。这是因为equals
方法比较的是字符串的内容,即使这些字符串的对象引用不同,只要它们的内容相等,就认为它们是相等的。
接口和抽象类有什么共同点和区别?
接口和抽象类的共同点
-
实例化:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
-
抽象方法:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。
接口和抽象类的区别
-
设计目的:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
-
继承和实现:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。
-
成员变量:接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(private
,protected
,public
),可以在子类中被重新定义或赋值。
接口和抽象类有什么共同点和区别?
接口和抽象类的共同点
-
实例化:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
-
抽象方法:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。
接口和抽象类的区别
-
设计目的:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
-
继承和实现:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。
-
成员变量:接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(private
,protected
,public
),可以在子类中被重新定义或赋值
Object 类的常见方法有哪些?
Object 类是一个特殊的类,是所有类的父类,主要提供了以下 11 个方法:
getclass hashcode哈希码的作用是确定该对象在哈希表中的索引位置 equals clone toString notify唤醒监视器 wait finalize回收
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
总结:
-
equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。 -
两个对象有相同的
hashCode
值,他们也不一定是相等的(哈希碰撞)。
String、StringBuffer、StringBuilder 的区别?
可变性
String
是不可变的(后面会详细分析原因)。
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用 final
和 private
关键字修饰,最关键的是这个 AbstractStringBuilder
类还提供了很多修改字符串的方法比如 append
方法。
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
-
操作少量的数据: 适用
String
-
单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
-
多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String 为什么是不可变的?
String
类中使用 final
关键字修饰字符数组来保存字符串
🐛 修正:我们知道被
final
关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final
关键字修饰的数组保存字符串并不是String
不可变的根本原因,因为这个数组保存的字符串是可变的(final
修饰引用类型变量的情况)。
String
真正不可变有下面几点原因:
保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。
String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。相关阅读:如何理解 String 类型值的不可变? - 知乎提问open in new window
补充(来自issue 675open in new window):在 Java 9 之后,
String
、StringBuilder
与StringBuffer
的实现改用byte
数组存储字符串。
Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
-
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。 -
Error
:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Throwable 类常用方法有哪些?
-
String getMessage()
: 返回异常发生时的简要描述 -
String toString()
: 返回异常发生时的详细信息 -
String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同 -
void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
什么是泛型?有什么作用?
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList
对象只能传入 Person
对象,如果传入其他类型的对象就会报错。
项目中哪里用到了泛型?
-
自定义接口通用返回结果
CommonResult<T>
通过参数T
可根据具体的返回类型动态指定结果的数据类型 -
定义
Excel
处理类ExcelUtil<T>
用于动态指定Excel
导出的数据类型 -
构建集合工具类(参考
Collections
中的sort
,binarySearch
方法)
反射是什么
Java的反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制。
这样就能使用所有的类和对象,包括不是你自己编写的,或者制定好流程,动态地输入获取不同输出,和泛型的思想类似。虽然这样方便,但也会带来安全问题。
何谓注解?
可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了Annotation
的特殊接口
注解的解析方法有哪几种?
注解只有被解析之后才会生效,常见的解析方法有两种:
-
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 -
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
-
序列化:将数据结构或对象转换成二进制字节流的过程
-
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
I/O 流为什么要分为字节流和字符流呢?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
个人认为主要有两点原因:
-
字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时;
-
如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。
语法糖
什么是语法糖?
语法糖(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。