Java基础小结
# Java 基础小结
# Java 基础概念与常识
# 什么是 Java ?
Java 是 1995
年由 sun
公司推出的一门高级语言。
Java 的四个基本特性是面向对象、平台无关性、安全性和简单性。
具体特点如下:
简单易学。
平台无关性。
面向对象
面向对象是一种程序设计技术,以木匠工作为例,使用面向对象方式实现的木匠的工作关注重点永远是制作椅子,其次才是工具。
而面向过程则优先关注制作工具。
与
C++
不同的是,Java
不支持多继承,取而代之的是更加简单的接口的概念。面向对象三大特性: 封装、多态、继承。
编译与解释并存
可靠性
Java
通过早期检测以及运行时检测消除了容易出错的情况。- 与
C++
不同的是,C++
在操作数组、字符串方式上利用指针模型避免了重写内存或者损坏数据的问题。
安全性: Java 适用于网络/分布式环境,为了达到这个目标,Java 在防病毒,防篡改做出很大的努力。
支持网络编程并且非常方便。
支持多线程。
什么是【编译型】语言和【解释型】语言?
编译型:编译型语言 (opens new window) 会通过编译器 (opens new window)将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
解释型:解释型语言 (opens new window)会通过解释器 (opens new window)一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
而 Java 是编译与解释并存,因为
java
源代码运行时需要先编译成为字节码文件(.class)
,然后再通过解释器翻译成机器码运行。
# Java 的三种技术架构
JavaSE
:标准版,即学生时期学习时使用的版本。JavaEE
:web
开发采用的技术架构。JavaME
:为嵌入式设备提供的解决方案
# 什么是 JVM ?
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。
JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
字节码和不同系统的 JVM 实现是 Java 语言 “一次编译,随处可以运行” 的关键所在。
JVM 并不是只有一种!只要遵守 JVM
设计规范就能开发出自己所需要的 Java
虚拟机,我们日常所用的 HotSpot VM
只是其中一种实现而已。
# 什么是 JDK ?
JDK(Java Development Kit) 是 Java
开发工具包,包含了 JRE
所有的东西,所以作为开发人员,只需要安装 JDK
即可。
# 什么是 JRE ?
JRE(Java Runtime Environment) 是 Java
运行环境,包含运行所需要的类库以及 JVM。
你可能认为如果仅仅要运行 Java
程序,安装 JRE
即可,但是某些 web
程序例如:需要将 JSP
转换为 Java servlet
就需要 jdk
编译了,所以保守起见,无论运行还是开发,我们都建议在操作系统上安装 JDK
。
# Java 与 C++ 的区别
Java
没有指针的概念,不能像C++
一样直接操作内存,所以更加安全。Java
不支持多继承,但是可以通过多接口实现多继承。Java
只支持方法重载,不像C++
一样可以运算符重载。Java
有自动内存管理垃圾回收机制(GC)
,无需像C++
一样手动释放。
什么是 GC ?
# 基本语法
# 注释有哪几种形式?
有三种:
- 单行注释:通常用于解释方法内某单行代码的作用。(
//
) - 多行注释:通常用于解释一段代码的作用。(
/* */
) - 文档注释:通常用于生成 Java 开发文档。(即
java doc
)
# 标识符和关键字的区别是什么?
标识符
:简单来说就是一个名字,比如 某个店铺名。
关键字
:被赋予特殊含义的标识符,比如 警察局,医院。
注意:所有关键字都是小写,在 IDE 中会以特殊颜色展示。
# Java 有哪些关键字?
# 访问控制关键字解析
它们的作用是控制类、方法和变量的访问权限。
Java
中的访问控制关键字主要有以下四个:
public
: 表示公共的,任何地方都可以访问。在同一项目中或其他项目中,都可以通过引入类或模块进行访问。protected
: 表示受保护的,只有本类和其子类以及同一包中的其他类可以访问。在其他包中的子类不可以访问。default
(即不写访问控制符): 表示默认的,只有本类和同一包中的其他类可以访问,其他包中的类都不可以访问。private
: 表示私有的,只有本类中可以访问,其他类都不可以访问。
在
枚举类
中,为什么说访问控制修饰符
对于变量
来说是冗余的?因为在 枚举类型 的 成员变量 默认访问级别是
private
。具体来说:
- 封装性:枚举类型的成员变量通常封装了枚举实例的状态。由于枚举的固定性和不变性,成员变量通常需要是
private
的,以保证枚举实例的安全性和不可变性。- 不变性:枚举类的一个关键特性是其不变性。一旦枚举实例被创建,它的值就不能被修改。因此,枚举中的变量通常设置为
private final
,以确保它们在初始化后不能被更改。- 构造器限制:枚举类只能通过它的构造器来创建实例,而且枚举的构造器默认是
private
的。这意味着枚举的构造过程被严格控制,只有枚举类自身可以创建枚举实例。- 序列化:枚举类是可序列化的,它们实现了
java.io.Serializable
接口。序列化机制要求枚举的状态是固定的,因此变量的值在序列化后不能改变。- 简洁性:在枚举类中声明变量时,通常不需要指定访问控制修饰符。如果省略访问控制修饰符,这些变量默认就是
private
的,这符合枚举的封装和不变性原则。- Java语言规范:根据 Java 语言规范,枚举类型的成员变量默认访问级别是
private
。这是语言设计的一部分,旨在简化枚举的使用。- 方法使用:枚举类通常通过公共的静态方法来访问枚举常量,这些方法返回枚举常量的引用。因此,即使变量是
private
的,也可以通过这些公共方法来访问它们。- 单例模式:枚举类型在逻辑上实现了单例模式,每个枚举常量都是唯一的。因此,枚举常量的成员变量也应该是唯一的,并且通过枚举本身的保护机制来确保这一点。
可见性
同一个类 -> 同一个包 -> 子类 -> 全局范围
可见性 | private | default | protected | public |
---|---|---|---|---|
同一个类中 | ✔️ | ✔️ | ✔️ | ✔️ |
同一个包中 | ❌ | ❌ | ✔️ | ✔️ |
子类中 | ❌ | ❌ | ✔️ | ✔️ |
全局范围 | ❌ | ❌ | ❌ | ✔️ |
# final 关键字解析
final
是 Java 中的一个关键字,可以用来修饰类、方法和变量,表示它们不可被修改。
被
final
关键字修饰会怎么样?
final
修饰类:表示该类是不可继承的,即不能有子类。final
修饰方法:表示该方法不能被子类重写,即不能被修改。final
修饰变量:表示该变量是一个常量,只能被赋值一次,不能被修改。
final
关键字的主要作用如下:
- 提高代码的安全性:
final
关键字可以保证类、方法和变量在程序运行时不被修改,从而提高了代码的安全性和可维护性。 - 提高代码的性能:
final
关键字可以使得编译器在编译时进行优化,从而提高了代码的性能。 - 明确代码的含义:
final
关键字可以使得代码的含义更加明确,从而方便代码的维护和理解。
# if 和 else if
if
和 else if
是用于条件判断的控制结构。它们用于在不同的条件下执行不同的代码块。
基本的语法如下:
if (condition1) {
// 如果 condition1 为真,执行这里的代码块
} else if (condition2) {
// 如果 condition1 为假,而 condition2 为真,执行这里的代码块
} else {
// 如果前面的条件都为假,执行这里的代码块
}
2
3
4
5
6
7
8
在执行时,首先判断 condition1
是否为真。如果为真,执行 if
代码块;如果为假,继续判断 condition2
。如果 condition2
为真,执行 else if
代码块;如果 condition2
为假,执行 else
代码块。
重要的一点是,一旦某一个条件为真,后续的 else if
或 else
都不会再执行,因为 Java 中的 if - else if - else
结构是互斥的。
以下是一个简单的例子:
int number = 10;
if (number > 0) {
System.out.println("Number is positive");
} else if (number < 0) {
System.out.println("Number is negative");
} else {
System.out.println("Number is zero");
}
// 输出 Number is positive
2
3
4
5
6
7
8
9
10
11
# 变量
# 成员变量与局部变量的区别?
主要是四个区别:
从语法形式上看:
- 成员变量是属于类的,
- 而局部变量是在代码块或方法中定义的变量或是方法的参数。
- 成员变量可以被
public
,private
,static
等修饰符所修饰, - 而局部变量不能被访问控制修饰符及
static
所修饰; - 但是,成员变量和局部变量都能被
final
所修饰。
从变量在内存中的存储方式来看:
如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用
static
修饰,这个成员变量是属于实例的。对象存在于堆内存,是【类的实例化】
局部变量则存在于栈内存,是【在方法里定义的】
从变量在内存中的生存时间上看:
成员变量是对象的一部分,它随着对象的创建而存在,
而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
从变量是否有默认值来看:
- 成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值), - 而局部变量则不会自动赋值。(Java 编译器不会对局部变量进行默认初始化,因为这些变量的值只在方法或代码块中使用,没有默认值。如果程序员没有显式地初始化局部变量,则编译器会在编译时抛出错误。即使定义包装类型的局部变量也一样)
- 成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
# 静态变量有什么用?
静态变量也就是被 static
关键字修饰的变量。
它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存(属于类,只加载一下),即使创建多个对象,这样可以节省内存。
静态变量是通过类名来访问的,例如 StaticVariableExample.staticVar
(如果被 private
关键字修饰就无法这样访问了)。
public class StaticVariableExample {
// 静态变量
public static int staticVar = 0;
}
2
3
4
通常情况下,静态变量会被 final
关键字修饰成为常量。
public class ConstantVariableExample {
// 常量
public static final int constantVar = 0;
}
2
3
4
# 字符型常量和字符串常量的区别?
- 形式:
- 字符常量是单引号引起的一个字符,
- 字符串常量是双引号引起的 0 个或若干个字符。
- 含义:
- 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;
- 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小:
- 字符常量(
char
)只占 2 个字节; - 字符串常量(
String
)占若干个字节。
- 字符常量(
注意
char
在 Java 中占两个字节。
# 方法
# 什么是方法的返回值?方法有哪几种类型?
方法的返回值 是指我们获取到的某个方法体中的代码执行后产生的结果!
有四种类型:
- 无参数无返回值的方法
- 有参数无返回值的方法
- 有返回值无参数的方法
- 有返回值有参数的方法
# 静态方法为什么不能调用非静态成员?
这是因为
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
# 静态方法和实例方法有何不同?
1、调用方式
在外部调用静态方法时,可以使用
类名.方法名
的方式,也可以使用对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
2、访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),
而实例方法不存在这个限制。
# 重载和重写有什么区别?
重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等 |
访问修饰 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
# 什么是可变长参数?
可变长参数是指在函数或方法中,参数的数量是可变的,即函数或方法可以接受不确定数量的参数。
可变长参数必须放在参数列表的最后一个位置,并且使用省略号(...
)来表示。
例如:
public static int sum(int... nums) {
int result = 0;
for (int num : nums) {
result += num;
}
return result;
}
int sum1 = sum(1, 2, 3); // sum1 = 6
int sum2 = sum(1, 2, 3, 4); // sum2 = 10
int sum3 = sum(1); // sum3 = 1
int sum4 = sum(); // sum4 = 0
2
3
4
5
6
7
8
9
10
11
12
也可传入数组
int[] nums = {1, 2, 3};
int sum5 = sum(nums); // sum5 = 6
2
遇到方法重载时会优先匹配固定参数还是可变参数的方法呢?
会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
# 注解
# 谈谈对 Java 注解的理解,解决了什么问题?
注解可以看作是一种特殊的注释,本质上是继承了 Annotation
这一特殊接口,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解只有被解析之后才会生效。我们可以使用 JDK
提供的内置注解也可以自定义注解。
Java
注解的出现主要是为了解决代码中大量重复性工作,例如:配置文件的读取、日志记录、数据校验等。可以帮助开发者更加方便地管理和维护代码(还可以实现一些特定的功能),提高程序的质量(和开发效率)。
# 注解的解析方法有哪几种?
常见的解析方法有两种:
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。定义一个注解
MyAnnotation
:@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyAnnotation { String value(); }
1
2
3
4
5在代码中使用注解,并通过反射机制获取注解信息:
@MyAnnotation("hello") public class MyClass { public static void main(String[] args) { MyClass obj = new MyClass(); Class<?> clazz = obj.getClass(); MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class); System.out.println(annotation.value()); // 输出 "hello" } }
1
2
3
4
5
6
7
8
9字节码注解解析:在类加载期间,通过 ASM 或 Javassist 等字节码操作库来解析注解信息,并修改字节码文件。这种方式可以在不改变源代码的情况下,对代码进行动态的修改和增强。
定义一个注解
MyAnnotation
:@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyAnnotation { String value(); }
1
2
3
4
5使用 ASM 操作库在类加载期间解析注解信息:
public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = loadClassData(name); Class<?> clazz = defineClass(name, bytes, 0, bytes.length); // 解析注解信息 return clazz; } private byte[] loadClassData(String name) { // 读取类字节码文件 } } MyClassLoader loader = new MyClassLoader(); Class<?> clazz = loader.loadClass("com.example.MyClass"); MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class); System.out.println(annotation.value()); // 输出 "hello"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# SPI
# 什么是 Java 的 SPI 机制?
Java 的 SPI(Service Provider Interface)机制是一种用于动态加载和扩展服务的机制,它通过定义服务接口、服务提供者接口和加载配置文件的方式,实现了在运行时动态加载服务提供者实现的功能。
# SPI 机制的优缺点是什么?
SPI 机制的
- 优点是可以在不修改代码的情况下,动态地扩展应用程序的功能,提高了程序的灵活性和可扩展性。
- 缺点是容易发生冲突和重复加载等问题,需要谨慎使用。
# 如何在 Java 中使用 SPI 机制?
在 Java 中使用 SPI 机制需要完成以下步骤:
- 定义服务接口和服务提供者接口;
- 编写服务提供者实现,并将实现类打成 jar 包;
- 在 META-INF/services 目录下创建一个名为服务接口全限定名的配置文件,文件内容为服务提供者接口的全限定类名;
- 在程序中加载服务提供者实现,可以通过 ClassLoader 和反射机制实现。
# 如何避免 SPI 机制中的冲突问题?
为了避免 SPI 机制中的冲突问题,可以使用类加载器隔离机制,即创建多个类加载器,每个类加载器加载不同的 jar 包和配置文件,从而实现服务提供者实现的隔离。同时,也可以规范命名空间的使用,避免不同的服务提供者实现使用相同的命名空间。
# SPI 机制和 Spring 的 BeanFactory 有什么区别?
SPI 机制和 Spring 的 BeanFactory 都是用于实现插件化和扩展性的机制,但是它们的实现方式不同。SPI 机制是基于接口和配置文件的方式实现的,而 Spring 的 BeanFactory 是基于依赖注入和反射机制实现的。SPI 机制更加轻量级和灵活,适用于简单的应用场景,而 Spring 的 BeanFactory 更加强大和复杂,适用于大型的企业级应用。
# 序列化和反序列化
# 什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
序列化的主要目的是:
通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
# 序列化协议对应于 TCP/IP 4 层模型的哪一层?
- OSI 模型中的表示层
- TCP/IP 4 层模型中的应用层
# 项目中,JSON 数据的传输体现在哪?
在项目中,JSON 数据的传输是非常常见的方式。JSON 是一种轻量级的数据交换格式,可以方便地在前后端之间传输数据。
以下是一些常见的在项目中使用 JSON 传输数据的场景:
前后端 API 交互:在前后端分离的项目中,前端和后端通常通过 API 进行交互。在这种情况下,后端将数据以 JSON 格式返回给前端,前端可以使用 JavaScript 解析 JSON 数据,将其转换为对象,并在页面上渲染数据。
RESTful API:RESTful API 是一种基于 HTTP 协议的 API 设计风格,通常使用 JSON 作为数据传输格式。在 Java 项目中,可以使用 Spring MVC 或 JAX-RS 等框架来构建 RESTful API,并使用一些 JSON 解析库将 JSON 请求和响应转换为 Java 对象。
WebSocket 通信:在前后端分离的实时通信应用中,WebSocket 是一种常见的通信协议。在这种情况下,前后端可以使用 JSON 作为通信协议,将消息以 JSON 格式传输。
静态资源加载:在前后端分离的项目中,前端通常使用
AJAX
或Fetch API
从后端获取数据。在这种情况下,后端可以将数据以 JSON 格式返回给前端,前端可以使用 JavaScript 解析 JSON 数据,并在页面上渲染数据。
# 语法糖
# 什么是语法糖?
语法糖(Syntactic sugar)代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。
实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。如果你去看
com.sun.tools.javac.main.JavaCompiler
的源码,你会发现在compile()
中有一个步骤就是调用desugar()
,这个方法就是负责解语法糖的实现的。
# Java 中有哪些常见的语法糖?
Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。
# 内部类了解吗?
内部类是指定义在类内部的类,它可以访问外部类的私有变量和方法,从而实现对外部类的访问和控制。
内部类主要分为以下几种:
- **成员内部类:**定义在类中的普通内部类,可以访问外部类的私有变量和方法;(类中类)
- **静态内部类:**定义在类中的静态内部类,不能访问外部类的非静态变量和方法;(静态的类中类)
- 局部内部类:定义在方法内的内部类,只能在方法内部使用。(方法中的类)
# 匿名内部类了解吗?
匿名内部类是指没有名字的内部类,它是一种简化的内部类语法,可以用来创建一个临时的、只使用一次的类。
匿名内部类通常用于实现接口或抽象类(花括号中的内容是匿名内部类的具体实现),匿名内部类可以使得代码更加简洁,但也会使得代码的可读性和可维护性降低,因此需要谨慎使用。
例如,下面的代码演示了如何使用匿名内部类实现一个 Runnable
接口:
Thread t = new Thread(new Runnable() {
public void run() {
// 线程执行的代码
}
});
t.start();
2
3
4
5
6