Java基础
概念
Java的特点
- 平台无关性:Java编写一次,运行无处不在。Java编译器将源代码编译成字节码,可以在任何安装了JVM的系统上运行。
- 面向对象:Java是一门严格的面向对象的编程语言,几乎一切都是对象。面向对象编程(OOP)特性使得代码更易于维护和重用,包括类、对象、继承、多态、抽象和封装。
- 内存管理:Java有自己的垃圾回收机制,自动管理内存和回收不再使用的对象。开发者不需要手动管理内存,从而减少内存泄漏和其他内存相关问题。
Java为什么是跨平台的
主要依赖于JVM。JVM也是一个软件,不同的平台有不同的版本。编写的Java源码编译后会生成一种.class文件,称为字节码文件。JVM就是负责将字节码文件翻译成特定平台下的机器码然后运行。只要在不同平台上安装对应的JVM就可以运行字节码文件,运行编写的Java程序。
(不同平台下编译生成的字节码是一样的,但是由 JVM编译成的机器码却不一样)
(跨平台的是 Java程序,不是 JVM。JVM是用 C/C++开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM。)

JVM是什么
JVM是 java 虚拟机,主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是Java能够“一次编译,到处运行的”原因。
JVM、JDK、JRE三者的关系

- JVM是Java虚拟机,是Java程序运行的环境,负责将Java字节码(由Java编译器生成)解释或编译成机器码,并执行程序。JVM提供了内存管理、垃圾回收、安全性等功能,使得Java程序具备跨平台性。
- JDK是Java开发工具包,是开发Java程序所需的工具集合。包含了JVM、编译器(javac)、调试器(jdb)等开发工具,以及一系列的类库(如Java标准库和开发工具库)。JDK提供了开发、编译、调试和运行Java程序所需的全部工具和环境。
- JRE是Java运行时的环境,是Java程序运行所需的最小环境。它包含了JVM和一组Java类库,用于支持Java程序的执行。JRE不包含开发工具,只提供Java程序运行所需的运行环境。
为什么Java解释和编译都有
首先在Java经过编译之后生成字节码文件,接下来进入JVM中,就有两个步骤——编译和解释。

编译性:
- Java源代码首先被编译成字节码,JIT会把编译过的机器码保存起来,以备下次使用。
解释性:
- JVM中有一个方法调用计数器,当累计计数大于一定值时,就使用JIT进行编译生成机器码文件,否则就使用解释器进行解释执行,然后字节码也是经过解释器进行解释运行的。
所以Java既是编译性也是解释性语言,默认采用解释器和编译器混合的模式。
编译型语言和解释型语言的区别?
编译型语言和解释型语言的区别在于:
- 编译型语言:在程序执行之前,整个源代码会被编译成机器码或者字节码,生成可执行文件。执行时直接运行编译后的代码,速度快,但跨平台性较差。
- 解释型语言:在程序执行时,逐行解释执行源代码,不生成独立的可执行文件。通常由解释器动态解释并执行代码,跨平台性好,但执行速度相对较慢。
- 典型的编译型语言如C、C++,典型的解释型语言如Python、JavaScript。
Python和Java区别是什么?
- Java是一种已编译的编程语言,Java编译器将源代码编译为字节码,而字节码则由Java虚拟机执行
- python是一种解释语言,翻译时会在执行程序的同时进行翻译。
数据类型
八种基本数据类型
Java支持数据类型分为:基本数据类型和引用数据类型
基本数据类型包括:
- 数值型:整数类型(byte、short、int、long)和浮点类型(float、double)
- 字符型:char
- 布尔型:boolean


注意:
- java八种基本数据类型的字节数:1字节(byte、boolean)、 2字节(short、char)、4字节(int、float)、8字节(long、double)
- 浮点数的默认类型为double(如果需要声明一个常量为float型,则必须要在末尾加上f或F)
- 整数的默认类型为int(声明Long型在末尾加上l或者L)
- 八种基本数据类型的包装类:除了char的是Character、int类型的是Integer,其他都是首字母大写
- char类型是无符号的,不能为负,所以是0开始的
数据类型转换方式

- 自动类型转换(隐式转换):当目标类型的范围大于源类型时,Java会自动将源类型转换为目标类型,不需要显式的类型转换。例如,将int转换为long、将float转换为double等。
- 强制类型转换(显式转换):当目标类型的范围小于源类型时,需要使用强制类型转换将源类型转换为目标类型。这可能导致数据丢失或溢出。例如,将long转换为int、将double转换为int等。语法为:目标类型 变量名 = (目标类型) 源类型。
- 字符串转换:Java提供了将字符串表示的数据转换为其他类型数据的方法。例如,将字符串转换为整型int,可以使用Integer.parseInt()方法;将字符串转换为浮点型double,可以使用Double.parseDouble()方法等。
- 数值之间的转换:Java提供了一些数值类型之间的转换方法,如将整型转换为字符型、将字符型转换为整型等。这些转换方式可以通过类型的包装类来实现,例如Character类、Integer类等提供了相应的转换方法。
类型互转会出现什么问题
- 数据丢失:大类型转小类型,可能截断后高位丢失,如long转int
- 数据溢出:如double转int
- 精度损失:浮点数类型转换,如float转double
- 类型不匹配
为什么用bigDecimal而不用double
double执行二进制浮点运算,会出现精度丢失,Decimal是精确运算,一般用于金钱的运算。
装箱和拆箱
装箱(Boxing)和拆箱(Unboxing)是将基本数据类型和对应的包装类之间进行转换的过程。
eg. Integer i = 10;//装箱
int n = i;//拆箱
自动装箱主要发生在两种情况,一种是赋值时,另一种是在方法调用的时候。
赋值时:
最常见的一种情况,Java1.5前需要手动转换,现在所有转换由编译器完成。
方法调用时:
可以传入原始数据值或者对象,同样编译器会帮我们转换。
1 | public static Integer show(Integer iParam){ |
自动装箱的弊端:
如果在一个循环中进行自动装箱操作,下面的例子会创建多个多余的对象,影响性能。
1 | Integer sum = 0; |
sum进行自动拆箱,然后数值相加,最好自动装箱转换成Integer对象。所以循环中会创建4000个无用的Integer对象,会降低程序性能并且加重垃圾回收的工作量。所以应该正确声明变量类型,避免自动装箱引起的性能问题。
Java为什么要有Integer
Integer是int类型的包装类,就是把int类型包装成Object对象,对象封装有很多好处,可以把属性(数据)跟处理这些数据的方法结合在一起。(比如Integer中的parseInt()等方法)。
Java绝大部分方法或类都是用来处理类类型对象的。
泛型中:只能使用引用类型,而不能使用基本类型。如果在泛型中使用int类型,就必须使用Integer包装类。(例如:Collections.sort()内部放Integer而不是int。)
转换中:基本类型和引用类型不能直接进行转换,必须使用包装类来实现。(例如,int不能直接转成String,需要先转换成Integer类型,再转成String类型。)
集合中:Java集合只能存储对象,而不能存储基本数据类型。(例如,ArrayList集合类就只能以类作为他的存储对象,如果用Integer包装类,可以直接使用stream()方法来计算所有元素的和:int sum = list.stream().mapToInt(Integer::intValue).sum();)
Integer与int的区别
- 类型:int是基本数据类型,Integer是引用类型。前者是预定义的,不需要实例化就可以使用,而后者需要通过实例化对象来使用。意味着前者不需要额外的内存分配,而后者必须为对象分配内存。性能方面,前者通常较快。
- 自动装箱与拆箱:可以自动装箱和拆箱。
- 空指针异常:int可以直接赋值为0,而Integer必须实例化对象来赋值,否则操作时会出现空指针异常,因为被赋予了null值,而null值无法自动拆箱。
为什么要保留int类型
包装类是引用类型,对象的引用和对象本身是分开存储的,而对于基本类型数据,变量对应的内存块直接存储数据本身,所以基本数据类型读写效率更高。此外,在64位JVM上,在开启引用压缩的情况下,一个Integer对象占用16个字节的内存方面,而一个int类型数据只占用4字节的内存空间。所以读写效率和存储效率基本类型都比包装类高效。
Integer的缓存
Java的Integer类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的Integer对象。默认情况下这个范围是-128-127,当通过Integer.valueOf(int)方法创建一个在这个范围内的整数对象时,并不会每次都生成新的对象实例,而是复用缓存中的现有对象,会直接从内存中取出,不需要新建一个对象。
面向对象
怎么理解面向对象
面向对象是一种编程范式,将现实世界中的事物抽象成对象,对象具有属性(称为字段或属性)和行为(称为方法)。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的性能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。
面向对象三大特征
- 封装:将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。目的是增强安全性和简化编程,使对象更加独立。
- 继承:使得子类自动共享父类数据结构和方法的机制。是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。
- 多态:允许不同类的对象对同一消息作出响应,即同一个接口,使得不同的实例而执行不同操作、可分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。
多态体现在哪几个方面
- 方法重载:指同一个类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同)。虽然方法名相同,但传入的参数不同,编译器会在编译时确定调用哪个方法。
- 示例:对于一个add方法,可以定义为add(int a, int b)和add(double a, double b)。
- 方法重写:指子类能够提供父类中同名方法的具体实现。在运行时,JVM会根据对象的实际类型确定调用哪个版本的方法,这是实现多态的主要方式。
- 示例:在一个动物类中,定义一个sound方法,子类Dog可以重写该方法以实现bark,而Cat可以实现meow。
- 接口与实现:多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法,使得程序在面对不同具体实现时保持一贯的调用方式。
- 示例:多个类(如Dog,cat)都实现了一个Animal接口,当用Animal类型的引用来调用makeSound方法时,会触发对应的实现。
- 向下转型和向上转型:向上转型:可以使用父类类型的引用指向子类对象,这样可以在运行时采用不同的子类实现;向下转型:父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免ClassCastException。
多态解决了什么问题
多态指子类可以替换父类,在实际的代码运行中调用子类的方法实现。
多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。比如策略模式、基于接口而非实现编程、依赖倒置原则、里氏替换原则、利用多态去掉冗长的if-else语句等。
面向对象的设计原则
- 单一职责原则(SRP):一个类只负责处理一种改变(一项职责)。
- 开放封闭原则(OCP):对扩展开放,对修改关闭。
- 里氏替换原则(LSP):子类可以扩展父类的功能,但不能改变父类原有的功能。子类继承父类时,除了添加新的方法完成新增功能外,尽量不要重写父类的方法。
- 接口隔离原则(ISP):客户端不应该依赖那些它不需要的接口,即接口应该小而专。
- 依赖倒置原则(DIP):高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
- 最少知识原则(迪米特法则)(Law of Demeter):一个对象应当对其他对象有尽可能少的了解。
- 组合/聚合复用原则:优先使用对象组合,而不是类继承。
重载和重写的区别
- 重载:同一个类中可以定义多个同名方法,它们具有不同的参数列表(参数类型、参数个数或参数顺序),编译器会根据调用时的参数类型决定调用哪个方法。
- 重写:子类重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过@Override注解来表示重写。
抽象类和普通类的区别
- 实例化:普通类可以直接使用new关键字来实例化对象;而抽象类不能直接实例化,只能被继承。
- 方法实现:普通类的方法可以有具体实现;而抽象类中没有方法体,必须在继承它的子类中被重写(除非子类也是抽象类)。
- 继承:一个类可以继承一个普通类,继承后的新类可以选择重写父类的方法,而且可以继承多个接口;而一个类只能继承一个抽象类,子类必须实现父类中的所有抽象方法,除非子类也是抽象类,但可以同时实现多个接口。
- 实现限制(用途):普通类可以被其他类继承和使用;而抽象类一般用于作为基类,被其他类继承和扩展使用。
Java抽象类和接口的区别
两者特点:
- 抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
- 接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java8以后可以有默认方法和静态方法)。适用于定义类的能力和功能。
区别:
- 实现方式:实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
- 方法方式:接口只有定义,不能有方法的实现,java1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可以在抽象类中实现。
- 访问修饰符:接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可以被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
- 变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态变量)。
抽象类能否加final修饰
不能。
Java中的抽象类是用来被继承的,而final修饰符用于禁止类被继承或方法被重写,因此,抽象类和final修饰符是互斥的,不能同时使用。
接口里面可以定义的方法
- 抽象方法:接口的核心部分,所有实现接口的类都必须实现这些方法。抽象方法默认是public和abstract,这些修饰符可以省略。
1 | public interface Animal { |
- 默认方法
默认方法是在 Java 8 中引入的,允许接口提供具体实现。实现类可以选择重写默认方法。
1 | public interface Animal { |
- 静态方法
静态方法也是在 Java 8 中引入的,它们属于接口本身,可以通过接口名直接调用,而不需要实现类的对象。
1 | public interface Animal { |
- 私有方法
私有方法是在 Java 9 中引入的,用于在接口中为默认方法或其他私有方法提供辅助功能。这些方法不能被实现类访问,只能在接口内部使用。
1 | public interface Animal { |
抽象类能否被实例化
不能。
所以不能使用new关键字直接创建一个抽象类的对象。抽象类的存在主要是为了被继承,它通常包含一个或多个抽象方法(由abstract关键字修饰且无方法体的方法),这些方法需要在子类中被实现。
抽象类可以有构造器,这些构造器在子类实例化时会被调用,以便进行必要的初始化工作。然而,这个过程并不是直接实例化抽象类,而是创建了子类的实例,间接地使用了抽象类的构造器。
例如:
1 | public abstract class AbstractClass { |
在这个例子中,ConcreteClass继承了AbstractClass并实现了抽象方法abstractMethod()。当我们创建ConcreteClass的实例时,AbstractClass的构造器被调用,但这并不意味着AbstractClass被实例化;实际上,我们创建的是ConcreteClass的一个对象。
简而言之,抽象类不能直接实例化,但通过继承抽象类并实现所有抽象方法的子类是可以被实例化的。
接口能否包含构造函数
在接口中,不可以有构造方法,在接口里写入构造方法时,编译器提示:Interfaces cannot have constructors,因为接口不会有自己的实例的,所以不需要有构造函数。
(构造函数就是初始化class的属性或者方法,在new的一瞬间自动调用。)
java的接口不能new所以不需要构造函数,根本就没法调用。
静态变量和静态方法
二者与类本身关联,而不是与类的实例(对象)关联。它们在内存中只存在一份,可以被类的所有实例共享。
静态变量(也称为类变量)是在类中使用static关键字声明的变量。它们属于类而不是任何具体的对象。主要的特点:
- 共享性:所有该类的实例共享同一个静态变量。如果一个实例修改了静态变量的值,其他实例也会看到这个更改。
- 初始化:静态变量在类被加载时初始化,只会对其进行一次分配内存。
- 访问方式:静态变量可以直接通过类名访问,也可以通过实例访问,但推荐使用类名。
示例:
1 | public class MyClass { |
静态方法是在类中使用static关键字声明的方法。类似于静态变量,静态方法也属于类,而不是任何具体的对象。主要的特点:
- 无实例依赖:静态方法可以在没有创建类实例的情况下调用。对于静态方法来说,不能直接访问非静态的成员变量或方法,因为静态方法没有上下文的实例。
- 访问静态成员:静态方法可以直接调用其他静态变量和静态方法,但不能直接访问非静态成员。
- 多态性:静态方法不支持重写(Override),但可以被隐藏(Hide)。
1 | public class MyClass { |
使用场景:
- 静态变量:常用于需要在所有对象间共享的数据,如计数器、常量等。
- 静态方法:常用于助手方法(utility methods)、获取类级别的信息或者是没有依赖于实例的数据处理
非静态内部类和静态内部类的区别
- 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
- 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
- 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
- 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。
- 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。
非静态内部类可以直接访问外部方法,编译器是怎么做到的
非静态内部类可以直接访问外部方法是因为编译器在生成字节码时会为非静态内部类维护一个指向外部类实例的引用。
这个引用使得非静态内部类能够访问外部类的实例变量和方法。编译器会在生成非静态内部类的构造方法时,将外部类实例作为参数传入,并在内部类的实例化过程中建立外部类实例与内部类实例之间的联系,从而实现直接访问外部方法的功能。
有一个父类和子类,都有静态的成员变量、静态构造方法和静态方法,在我new一个子类对象的时候,加载顺序是怎么样的
当实例化一个子类对象时,静态成员变量、静态构造方法和静态方法的加载顺序遵循以下步骤:
- 在创建子类对象之前,首先会加载父类的静态成员变量和静态代码块(构造方法无法被
static修饰,因此这里是静态代码块)。这个加载是在类首次被加载时进行的,且只会发生一次。 - 接下来,加载子类的静态成员变量和静态代码块。这一过程也只发生一次,即当首次使用子类的相关代码时。
- 之后,执行实例化子类对象的过程。这时会呼叫父类构造方法,然后是子类的构造方法。
具体加载顺序可以简要总结为:
- 父类静态成员变量、静态代码块(如果有)
- 子类静态成员变量、静态代码块(如果有)
- 父类构造方法(实例化对象时)
- 子类构造方法(实例化对象时)
示例代码:
1 | class Parent { |
输出结果:
1 | Parent static block |
深拷贝和浅拷贝
深拷贝和浅拷贝的区别

- 浅拷贝:只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。相当于只是创建了一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型字段,只是将引用复制到新对象中,两个对象指向同一个引用对象。
- 深拷贝:复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。相当于会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对象。
实现深拷贝的三种方法
- 实现 Cloneable 接口并重写 clone() 方法
这种方法要求对象及其所有引用类型字段都实现 Cloneable 接口,并且重写 clone() 方法。在 clone() 方法中,通过递归克隆引用类型字段来实现深拷贝。
1 | class MyClass implements Cloneable { |
- 使用序列化和反序列化
通过将对象序列化为字节流,再从字节流反序列化为对象来实现深拷贝。要求对象及其所有引用类型字段都实现 Serializable 接口。
1 | import java.io.*; |
- 手动递归复制
针对特定对象结构,手动递归复制对象及其引用类型字段。适用于对象结构复杂度不高的情况。
1 | class MyClass { |
泛型
什么是泛型
泛型是 Java 编程语言中的一个重要特性,它允许类、接口和方法在定义时使用一个或多个类型参数,这些类型参数在使用时可以被指定为具体的类型。
泛型的主要目的是在编译时提供更强的类型检查,并且在编译后能够保留类型信息,避免了在运行时出现类型转换异常。
为什么需要泛型
- 适用于多种数据类型执行相同的代码
1 | private static int add(int a, int b) { |
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法,如下:
1 | private static <T extends Number> double add(T a, T b) { |
- 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
示例:
1 | List list = new ArrayList(); |
我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现java.lang.ClassCastException异常。
引入泛型,它将提供类型的约束,提供编译前的检查:
1 | List<String> list = new ArrayList<String>(); |
对象
创建对象的方式
使用new关键字:通过new关键字直接调用类的构造方法来创建对象。
1 | MyClass obj = new MyClass(); |
使用Class类的newInstance()方法:通过反射机制,可以使用Class类的newInstance()方法创建对象。
1 | MyClass obj = (MyClass) Class.forName("com.example.MyClass").newInstance(); |
使用Constructor类的newInstance()方法:同样是通过反射机制,可以使用Constructor类的newInstance()方法创建对象。
1 | Constructor<MyClass> constructor = MyClass.class.getConstructor(); |
使用clone()方法:如果类实现了Cloneable接口,可以使用clone()方法复制对象。
1 | MyClass obj1 = new MyClass(); |
使用反序列化:通过将对象序列化到文件或流中,然后再进行反序列化来创建对象。
1 | // SerializedObject.java |
Java创建对象除了new还有什么方式
- 通过反射创建对象:通过 Java 的反射机制可以在运行时动态地创建对象。可以使用 Class 类的 newInstance() 方法或者通过 Constructor 类来创建对象。
1 | public class MyClass { |
- 通过反序列化创建对象:通过将对象序列化(保存到文件或网络传输)然后再反序列化(从文件或网络传输中读取对象)的方式来创建对象,对象能被序列化和反序列化的前提是类实现Serializable接口。
1 | import java.io.*; |
- 通过clone创建对象:所有 Java 对象都继承自 Object 类,Object 类中有一个 clone() 方法,可以用来创建对象的副本,要使用 clone 方法,我们必须先实现 Cloneable 接口并实现其定义的 clone 方法。
1 | public class MyClass implements Cloneable { |
new出的对象什么时候回收
由Java的垃圾回收器(Garbage Collector)负责回收。其工作是在程序运行过程中自动进行的,它会周期性地检测不再被引用的对象,并将其回收释放内存。
回收的算法:
- 引用计数法:个对象的引用计数为0时,表示该对象不再被引用,可以被回收。
- 可达性分析算法:从根对象(如方法区中的类静态属性、方法中的局部变量等)出发,通过对象之间的引用链进行遍历,如果存在一条引用链到达某个对象,则说明该对象是可达的,反之不可达,不可达的对象将被回收。
- 终结器:如果对象重写了finalize()方法,垃圾回收器会在回收对象之前调用finalize()方法,对象可以在finalize()方法中进行一些清理操作。但是由于终结器机制的执行时间不确定,可能会导致不可预测的问题,所以不推荐使用。
反射
什么是反射
在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
反射的特性
运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等。
动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过Class类的newInstance()方法或Constructor对象的newInstance()方法实现的。
动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过Method类的invoke()方法实现,允许你传入对象实例和参数值来执行方法。
访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过Field类的get()和set()方法完成的。

反射的应用场景
- 加载数据库驱动
我们的项目底层数据库有时是用mysql,有时用oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了,假设 com.mikechen.java.myqlConnection,com.mikechen.java.oracleConnection这两个类我们要用。
这时候我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序,如果是mysql则传入mysql的驱动类,而如果是oracle则传入的参数就变成另一个了。
1 | // DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver()); |
- 配置文件加载
Spring 框架的 IOC(动态加载管理 Bean),Spring通过配置文件配置各种各样的bean,你需要用到哪些bean就配哪些,spring容器就会根据你的需求去动态加载,你的程序就能健壮地运行。
Spring通过XML配置模式装载Bean的过程:
- 将程序中所有XML或properties配置文件加载入内存
- Java类里面解析xml或者properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
- 使用反射机制,根据这个字符串获得某个类的Class实例
- 动态配置实例的属性
配置文件
1 | className=com.example.reflectdemo.TestInvoke |
实体类
1 | public class TestInvoke { |
解析配置文件内容
1 | // 解析xml或properties里面的内容,得到对应实体类的字节码字符串以及属性信息 |
利用反射获取实体类的Class实例,创建实体类的实例对象,调用方法
1 | public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, ClassNotFoundException, InstantiationException { |
运行结果:
1 | TestInvoke |
注解
注解的原理
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。
我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。
注解的作用域
注解的作用域(Scope)指的是注解可以应用在哪些程序元素上,例如类、方法、字段等。Java注解的作用域可以分为三种:
类级别作用域:用于描述类的注解,通常放置在类定义的上面,可以用来指定类的一些属性,如类的访问级别、继承关系、注释等。
方法级别作用域:用于描述方法的注解,通常放置在方法定义的上面,可以用来指定方法的一些属性,如方法的访问级别、返回值类型、异常类型、注释等。
字段级别作用域:用于描述字段的注解,通常放置在字段定义的上面,可以用来指定字段的一些属性,如字段的访问级别、默认值、注释等。
除了这三种作用域,Java还提供了其他一些注解作用域,例如构造函数作用域和局部变量作用域。这些注解作用域可以用来对构造函数和局部变量进行描述和注释。
异常
介绍一下异常

- Error(错误):表示运行时环境的错误。错误是程序无法处理的严重问题,如系统崩溃、虚拟机错误、动态链接失败等。通常程序不应该捕获这类错误。例如,OutOfMemoryError、StackOverflowError等。
- Exception(异常):表示程序本身可以处理的异常事件。
- 非运行时异常:在编译时必须被捕获或者声明抛出。通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。这种异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。
- 运行时异常(RuntimeException):由程序错误导致,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。这种异常不需要在编译时强制捕获或声明。
Java异常处理有哪些
异常处理是通过使用try-catch语句块来捕获和处理异常。以下是Java中常用的异常处理方式:
- try-catch语句块:用于捕获并处理可能抛出的异常。try块中包含可能抛出异常的代码,catch块用于捕获并处理特定类型的异常。可以有多个catch块来处理不同类型的异常。
1 | try { |
- throw语句:用于手动抛出异常。可以根据需要在代码中使用throw语句主动抛出特定类型的异常。
1 | throw new ExceptionType("Exception message"); |
- throws关键字:用于在方法声明中声明可能抛出的异常类型。如果一个方法可能抛出异常,但不想在方法内部进行处理,可以使用throws关键字将异常传递给调用者来处理。
1 | public void methodName() throws ExceptionType { |
- finally块:用于定义无论是否发生异常都会执行的代码块。通常用于释放资源,确保资源的正确关闭。
1 | try { |
抛出异常为什么不用throws
如果异常是未检查异常或者在方法内部被捕获和处理了,那么就不需要使用throws。
- Unchecked Exceptions:未检查异常(unchecked exceptions)是继承自RuntimeException类或Error类的异常,编译器不强制要求进行异常处理。因此,对于这些异常,不需要在方法签名中使用throws来声明。示例包括NullPointerException、ArrayIndexOutOfBoundsException等。
- 捕获和处理异常:另一种常见情况是,在方法内部捕获了可能抛出的异常,并在方法内部处理它们,而不是通过throws子句将它们传递到调用者。这种情况下,方法可以处理异常而无需在方法签名中使用throws。
try-catch中的语句运行情况
try块中的代码将按顺序执行,如果抛出异常,将在catch块中进行匹配和处理,然后程序将继续执行catch块之后的代码。如果没有匹配的catch块,异常将被传递给上一层调用的方法。
try{return “a”} fianlly{return “b”}这条语句返回什么
finally块中的return语句会覆盖try块中的return返回,因此,该语句将返回”b”。
object
== 与 equals 有什么区别?
对于字符串变量来说,使用”==”和”equals”比较字符串时,其比较方法不同。”==”比较两个变量本身的值,即两个对象在内存中的首地址,”equals”比较字符串包含内容是否相同。
对于非字符串变量来说,如果没有对equals()进行重写的话,”==” 和 “equals”方法的作用是相同的,都是用来比较对象在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象。
- ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
- equals():比较的是两个字符串的内容,属于内容比较。
StringBuffer和StringBuild区别是什么?
区别:
- String 是 Java 中基础且重要的类,被声明为 final class,是不可变字符串。因为它的不可变性,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
- StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类。它提供了 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列。在很多情况下我们的字符串拼接操作不需要线程安全,所以 StringBuilder 登场了。
- StringBuilder 是 JDK1.5 发布的,它和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。
线程安全:
- StringBuffer:线程安全
- StringBuilder:线程不安全
速度:
- 一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。
使用场景:
- 操作少量的数据使用 String。
- 单线程操作大量数据使用 StringBuilder。
- 多线程操作大量数据使用 StringBuffer。

Java 1.8 新特性
stream的API
Java 8引入了Stream API,它提供了一种高效且易于使用的数据处理方式,特别适合集合对象的操作,如过滤、映射、排序等。Stream API不仅可以提高代码的可读性和简洁性,还能利用多核处理器的优势进行并行处理。
适用场景举例:
- 场景一:过滤并收集满足条件的元素
eg. 从一个列表中筛选出所有长度大于3的字符串,并收集到一个新的列表中。
没有Stream API的做法:
1 | List<String> originalList = Arrays.asList("apple", "fig", "banana", "kiwi"); |
这段代码需要显式地创建一个新的ArrayList,并通过循环遍历原列表,手动检查每个元素是否满足条件,然后添加到新列表中。
使用Stream API的做法:
1 | List<String> originalList = Arrays.asList("apple", "fig", "banana", "kiwi"); |
这里,我们直接在原始列表上调用.stream()方法创建了一个流,使用.filter()中间操作筛选出长度大于3的字符串,最后使用.collect(Collectors.toList())终端操作将结果收集到一个新的列表中。代码更加简洁明了,逻辑一目了然。
- 场景二:计算列表中所有数字的总和
eg. 计算一个数字列表中所有元素的总和。
没有Stream API的做法:
1 | List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); |
这个传统的for-each循环遍历列表中的每一个元素,累加它们的值来计算总和。
使用Stream API的做法:
1 | List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); |
通过Stream API,我们可以先使用.mapToInt()将Integer流转换为IntStream(这是为了高效处理基本类型),然后直接调用.sum()方法来计算总和,极大地简化了代码。
Stream流的并行API是什么?
是 ParallelStream。
并行流(ParallelStream)就是将源数据分为多个子流对象进行多线程操作,然后将处理的结果再汇总为一个流对象,底层是使用通用的 fork/join 池来实现,即将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。
Stream串行流与并行流的主要区别:

对CPU密集型的任务来说,并行流使用ForkJoinPool线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用ParallelStream并不是很好的选择。
completableFuture怎么用的?
CompletableFuture是由Java 8引入的,在Java8之前我们一般通过Future实现异步。
- Future用于表示异步计算的结果,只能通过阻塞或者轮询的方式获取结果,而且不支持设置回调方法,Java 8之前若要设置回调一般会使用guava的ListenableFuture,回调的引入又会导致臭名昭著的回调地狱(下面的例子会通过ListenableFuture的使用来具体进行展示)。
- CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题。
下面将举例来说明,我们通过ListenableFuture、CompletableFuture来实现异步的差异。假设有三个操作step1、step2、step3存在依赖关系,其中step3的执行依赖step1和step2的结果。
Future(ListenableFuture)的实现(回调地狱)如下:
1 | ExecutorService executor = Executors.newFixedThreadPool(5); |
CompletableFuture的实现如下:
1 | ExecutorService executor = Executors.newFixedThreadPool(5); |
显然,CompletableFuture的实现更为简洁,可读性更好。

CompletableFuture实现了两个接口(如上图所示):Future、CompletionStage。
- Future表示异步计算的结果,CompletionStage用于表示异步执行过程中的一个步骤(Stage),这个步骤可能是由另外一个CompletionStage触发的,随着当前步骤的完成,也可能会触发其他一系列CompletionStage的执行。
- 从而我们可以根据实际业务对这些步骤进行多样化的编排组合,CompletionStage接口正是定义了这样的能力,我们可以通过其提供的thenAppy、thenCompose等函数式编程方法来组合编排这些步骤。
序列化
怎么把一个对象从一个jvm转移到另一个jvm?
- 使用序列化和反序列化:将对象序列化为字节流,并将其发送到另一个 JVM,然后在另一个 JVM 中反序列化字节流恢复对象。这可以通过 Java 的 ObjectOutputStream 和 ObjectInputStream 来实现。
- 使用消息传递机制:利用消息传递机制,比如使用消息队列(如 RabbitMQ、Kafka)或者通过网络套接字进行通信,将对象从一个 JVM 发送到另一个。这需要自定义协议来序列化对象并在另一个 JVM 中反序列化。
- 使用远程方法调用(RPC):可以使用远程方法调用框架,如 gRPC,来实现对象在不同 JVM 之间的传输。远程方法调用可以让你在分布式系统中调用远程 JVM 上的对象的方法。
- 使用共享数据库或缓存:将对象存储在共享数据库(如 MySQL、PostgreSQL)或共享缓存(如 Redis)中,让不同的 JVM 可以访问这些共享数据。这种方法适用于需要共享数据但不需要直接传输对象的场景。
序列化和反序列化的缺点
Java 默认的序列化虽然实现方便,但却存在安全漏洞、不跨语言以及性能差等缺陷。
- 无法跨语言: Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
- 容易被攻击:Java 序列化是不安全的,我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。
- 序列化后的流太大:序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。
那么该如何实现
考虑用主流序列化框架,比如FastJson、Protobuf来替代Java序列化。
如果追求性能的话,Protobuf 序列化框架会比较合适,Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。
将对象转为二进制字节流具体怎么实现?
像序列化和反序列化,无论这些可逆操作是什么机制,都会有对应的处理和解析协议,例如加密和解密,TCP的粘包和拆包,序列化机制是通过序列化协议来进行处理的,和 class 文件类似,它其实是定义了序列化后的字节流格式,然后对此格式进行操作,生成符合格式的字节流或者将字节流解析成对象。
在Java中通过序列化对象流来完成序列化和反序列化:
- ObjectOutputStream:通过writeObject()方法做序列化操作。
- ObjectInputStrean:通过readObject()方法做反序列化操作。
只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!
实现对象序列化:
- 让类实现Serializable接口:
1 | import java.io.Serializable; |
- 创建输出流并写入对象:
1 | import java.io.FileOutputStream; |
实现对象反序列化:
- 创建输入流并读取对象:
1 | import java.io.FileInputStream; |
通过以上步骤,对象obj会被序列化并写入到文件”object.ser”中,然后通过反序列化操作,从文件中读取字节流并恢复为对象newObj。这种方式可以方便地将对象转换为字节流用于持久化存储、网络传输等操作。需要注意的是,要确保类实现了Serializable接口,并且所有成员变量都是Serializable的才能被正确序列化。
设计模式
volatile和sychronized如何实现单例模式
1 | public class SingleTon { |
正确的双重检查锁定模式需要需要使用 volatile。volatile主要包含两个功能。
- 保证可见性。使用 volatile 定义的变量,将会保证对所有线程的可见性。
- 禁止指令重排序优化。
由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
代理模式和适配器模式有什么区别?
- 目的不同:代理模式主要关注控制对对象的访问,而适配器模式则用于接口转换,使不兼容的类能够一起工作。
- 结构不同:代理模式一般包含抽象主题、真实主题和代理三个角色,适配器模式包含目标接口、适配器和被适配者三个角色。
- 应用场景不同:代理模式常用于添加额外功能或控制对对象的访问,适配器模式常用于让不兼容的接口协同工作。
I/O
Java怎么实现网络IO高并发编程?
可以用 Java NIO ,是一种同步非阻塞的I/O模型,也是I/O多路复用的基础。
传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据, 如果使用BIO要想要并发处理多个客户端的i/o,那么会使用多线程模式,一个线程专门处理一个客户端 io,这种模式随着客户端越来越多,所需要创建的线程也越来越多,会急剧消耗系统的性能。

NIO 是基于I/O多路复用实现的,它可以只用一个线程处理多个客户端I/O,如果你需要同时管理成千上万的连接,但是每个连接只发送少量数据,例如一个聊天服务器,用NIO实现会更好一些。

BIO、NIO、AIO区别是什么?
- BIO(blocking IO):就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
- NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
- AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
NIO是怎么实现的?
NIO是一种同步非阻塞的IO模型,所以也可以叫NON-BLOCKINGIO。同步是指线程不断轮询IO事件是否就绪,非阻塞是指线程在等待IO的时候,可以同时做其他任务。
同步的核心就Selector(I/O多路复用),Selector代替了线程本身轮询IO事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当IO事件就绪时,可以通过写到缓冲区,保证IO的成功,而无需线程阻塞式地等待。
NIO由一个专门的线程处理所有IO事件,并负责分发。事件驱动机制,事件到来的时候触发操作,不需要阻塞的监视事件。线程之间通过wait,notify通信,减少线程切换。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

有哪个框架用到NIO了吗?
Netty。
Netty 的 I/O 模型是基于非阻塞 I/O 实现的,底层依赖的是 NIO 框架的多路复用器 Selector。采用 epoll 模式后,只需要一个线程负责 Selector 的轮询。当有数据处于就绪状态后,需要一个事件分发器(Event Dispather),它负责将读写事件分发给对应的读写事件处理器(Event Handler)。事件分发器有两种设计模式:Reactor 和 Proactor,Reactor 采用同步 I/O, Proactor 采用异步 I/O。

Reactor 实现相对简单,适合处理耗时短的场景,对于耗时长的 I/O 操作容易造成阻塞。Proactor 性能更高,但是实现逻辑非常复杂,适合图片或视频流分析服务器,目前主流的事件驱动模型还是依赖 select 或 epoll 来实现。
其他
有一个学生类,想按照分数排序,再按学号排序,应该怎么做?
可以使用Comparable接口来实现按照分数排序,再按照学号排序。首先在学生类中实现Comparable接口,并重写compareTo方法,然后在compareTo方法中实现按照分数排序和按照学号排序的逻辑。
1 | public class Student implements Comparable<Student> { |
然后在需要对学生列表进行排序的地方,使用Collections.sort()方法对学生列表进行排序即可:
1 | List<Student> students = new ArrayList<>(); |
Native方法解释一下
在Java中,native方法是一种特殊类型的方法,它允许Java代码调用外部的本地代码,即用C、C++或其他语言编写的代码。native关键字是Java语言中的一种声明,用于标记一个方法的实现将在外部定义。
在Java类中,native方法看起来与其他方法相似,只是其方法体由native关键字代替,没有实际的实现代码。例如:
1 | public class NativeExample { |
要实现native方法,你需要完成以下步骤:
- 生成JNI头文件:使用javah工具从你的Java类生成C/C++的头文件,这个头文件包含了所有native方法的原型。
- 编写本地代码:使用C/C++编写本地方法的实现,并确保方法签名与生成的头文件中的原型匹配。
- 编译本地代码:将C/C++代码编译成动态链接库(DLL,在Windows上),共享库(SO,在Linux上)
- 加载本地库:在Java程序中,使用System.loadLibrary()方法来加载你编译好的本地库,这样JVM就能找到并调用native方法的实现了。




