概念

值传递和引用传递
- 值传递:传递的是实际值的副本,适用于基本数据类型(如
int、char等),修改方法内的参数副本,不会影响原变量的值 - 引用传递:本质仍然是值传递,传递的是对象引用的副本 Java 中所有参数传递都是值传递
数据类型
八种数据类型
- 基本数据类型
- 数值型:整数类型(byte、short、int、long)和浮点类型(float、double)
- 字符型:char
- 布尔型:boolean
- 引用数据类型
基本数据类型转换
自动类型转换:小范围转大范围
强制类型转换:大范围转小范围,可能导致:精度损失、数据溢出。转换前建议检查数据范围
对象引用转换
向上转型:安全、自动
向下转型:有风险,需手动。解决方法:用 instance of 检查
为什么用 bigDecimal 不用 double
double 会出现精度丢失的问题,double 执行的是二进制浮点运算,二进制有些情况下不能准确的表示一个小数,就像十进制不能准确的表示 1/3(1/3=0.3333…),也就是说二进制表示小数的时候只能够表示能够用 1/(2^n) 的和的任意组合,但是 0.1 不能够精确表示,因为它不能够表示成为 1/(2^n) 的和的形式。比如:
System.out.println(0.05 + 0.01);
System.out.println(1.0 - 0.42);
System.out.println(4.015 * 100);
System.out.println(123.3 / 100);
输出:
0.060000000000000005
0.5800000000000001
401.49999999999994
1.2329999999999999
我们手中有 0.06 元,却无法购买一个 0.05 元和一个 0.01 元的商品。因为如上所示,他们两个的总和为 0.060000000000000005。这无疑是一个很严重的问题,尤其是当电商网站的并发量上去的时候,出现的问题将是巨大的。可能会导致无法下单,或者对账出现问题。而 Decimal 是精确计算 , 所以一般牵扯到金钱的计算 , 都使用 Decimal。
我们手中有 0.06 元,却无法购买一个 0.05 元和一个 0.01 元的商品。因为如上所示,他们两个的总和为 0.060000000000000005。这无疑是一个很严重的问题,尤其是当电商网站的并发量上去的时候,出现的问题将是巨大的。可能会导致无法下单,或者对账出现问题。
拆箱和装箱
将基本数据类型和对应的包装类之间进行转换的过程
自动装箱有一个问题,那就是在一个循环中进行自动装箱操作的情况,如下面的例子就会创建多余的对象,影响程序的性能:
Integer sum = 0; for(int i=1000; i<5000; i++){ sum+=i; }
Integer 缓存
Java 的 Integer 类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的 Integer 对象。
默认情况下,这个范围是 -128 至 127。当通过 Integer.valueOf(int) 方法创建一个在这个范围内的整数对象时,并不会每次都生成新的对象实例,而是复用缓存中的现有对象,会直接从内存中取出,不需要新建一个对象。
面向对象
三大特性
Java 面向对象三大特性:封装、继承、多态
- 封装:封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。封装的目的是增强安全性和简化编程,使得对象更加独立。
- 继承:继承是一种可以使得子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。
- 多态:多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。
多态的体现
- 方法重载:同一类可以有多个同名方法
- 方法重写:子类重写父类同名方法
- 接口与实现:多个类可以实现同一接口,可以用接口类型调用这些类的方法
- 向上转型和向下转型:子类与父类转换
面向对象的设计原则
- 单一职责原则(SRP):一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。例子:考虑一个员工类,它应该只负责管理员工信息,而不应负责其他无关工作。
- 开放封闭原则(OCP):软件实体应该对扩展开放,对修改封闭。例子:通过制定接口来实现这一原则,比如定义一个图形类,然后让不同类型的图形继承这个类,而不需要修改图形类本身。
- 里氏替换原则(LSP):子类对象应该能够替换掉所有父类对象。例子:一个正方形是一个矩形,但如果修改一个矩形的高度和宽度时,正方形的行为应该如何改变就是一个违反里氏替换原则的例子。
- 接口隔离原则(ISP):客户端不应该依赖那些它不需要的接口,即接口应该小而专。例子:通过接口抽象层来实现底层和高层模块之间的解耦,比如使用依赖注入。
- 依赖倒置原则(DIP):高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。例子:如果一个公司类包含部门类,应该考虑使用合成/聚合关系,而不是将公司类继承自部门类。
- 最少知识原则 (Law of Demeter):一个对象应当对其他对象有最少的了解,只与其直接的朋友交互。
Java 抽象类和接口的区别
两者的特点:
- 抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
- 接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8 以后可以有默认方法和静态方法)。适用于定义类的能力或功能。 两者的区别:
- 实现方式:实现接口的关键字为 implements,继承抽象类的关键字为 extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
- 方法方式:接口只有定义,不能有方法的实现,java 1.8 中可以定义 default 方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
- 访问修饰符:接口成员变量默认为 public static final,必须赋初值,不能被修改;其所有的成员方法都是 public、abstract 的。抽象类中成员变量默认 default,可在子类中被重新定义,也可被重新赋值(抽象类不能加 final 修饰,Java 中的抽象类是用来被继承的,而 final 修饰符用于禁止类被继承或方法被重写);抽象方法被 abstract 修饰,不能被 private、static、synchronized 和 native 等修饰,必须以分号结尾,不带花括号。
- 变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态常量)。
接口里面可以定义的方法
- 抽象方法:抽象方法是接口的核心部分,所有实现接口的类都必须实现这些方法。抽象方法默认是 public 和 abstract,这些修饰符可以省略。
public interface Animal {
void makeSound();
}
- 默认方法:默认方法是在 Java 8 中引入的,允许接口提供具体实现。实现类可以选择重写默认方法。
public interface Animal {
void makeSound();
default void sleep() {
System.out.println("Sleeping...");
}
}
- 静态方法:静态方法也是在 Java 8 中引入的,它们属于接口本身,可以通过接口名直接调用,而不需要实现类的对象。
public interface Animal {
void makeSound();
static void staticMethod() {
System.out.println("Static method in interface");
}
}
- 私有方法:私有方法是在 Java 9 中引入的,用于在接口中为默认方法或其他私有方法提供辅助功能。这些方法不能被实现类访问,只能在接口内部使用。
public interface Animal {
void makeSound();
default void sleep() {
System.out.println("Sleeping...");
logSleep();
}
private void logSleep() {
System.out.println("Logging sleep");
}
}
抽象类可以实例化吗
抽象类主要是为了被继承,不能被实例化
抽象类可以有构造器,这些构造器在子类实例化时会被调用,以便进行必要的初始化工作。然而,这个过程并不是直接实例化抽象类,而是创建了子类的实例,间接地使用了抽象类的构造器。
关键字
final 的作用
- 修饰类:当
final修饰一个类时,表示这个类不能被继承,是类继承体系中的最终形态。例如,Java 中的String类就是用final修饰的,这保证了String类的不可变性和安全性,防止其他类通过继承来改变String类的行为和特性。 - 修饰方法:用
final修饰的方法不能在子类中被重写。比如,java.lang.Object类中的getClass方法就是final的,因为这个方法的行为是由 Java 虚拟机底层实现来保证的,不应该被子类修改。 - 修饰变量:当
final修饰基本数据类型的变量时,该变量一旦被赋值就不能再改变。例如,final int num = 10;,这里的num就是一个常量,不能再对其进行重新赋值操作,否则会导致编译错误。对于引用数据类型,final修饰意味着这个引用变量不能再指向其他对象,但对象本身的内容是可以改变的。例如,final StringBuilder sb = new StringBuilder("Hello");,不能让sb再指向其他StringBuilder对象,但可以通过sb.append(" World");来修改字符串的内容。
static 的作用
static 关键字主要用于修饰类的成员(变量、方法、代码块)和内部类,其核心作用是将成员与类本身关联,而非与类的实例(对象)关联。
静态代码块:在类加载时执行,且只执行一次(优于对象构造方法),用于初始化静态变量或执行类级别的预处理操作。多个静态代码块按定义顺序执行,且先于非静态代码块和构造方法。
深拷贝和浅拷贝
实现深拷贝三种方法
- 实现 Cloneable 接口并重写 clone() 方法 这种方法要求对象及其所有引用类型字段都实现 Cloneable 接口,并且重写 clone() 方法。在 clone() 方法中,通过递归克隆引用类型字段来实现深拷贝。
- 使用序列化和反序列化 通过将对象序列化为字节流,再从字节流反序列化为对象来实现深拷贝。要求对象及其所有引用类型字段都实现 Serializable 接口
- 手动递归复制
泛型
泛型允许类、接口和方法在定义时使用一个或多个类型参数,这些类型参数在使用时可以被指定为具体的类型。
对象
创建方式
| 方式 | 核心原理 | 是否调用构造器? | 特点与应用场景 |
|---|---|---|---|
new 关键字 | JVM 指令 | 是 | 最标准、最常用,紧密耦合 |
| 反射 | 运行时类信息 | 是 (Constructor) | 灵活,解耦,用于框架 |
clone() | 复制现有对象 | 否 | 基于原型创建副本,需实现 Cloneable,注意 Object.clone() 默认是浅拷贝 |
| 反序列化 | 从字节流恢复 | 否 | 用于持久化和网络通信,需实现 Serializable |
| 工厂模式 | 方法封装 new | 是 (在方法内) | 解耦,隐藏创建逻辑,控制实例 |
Java 对象回收
- 引用计数法:某个对象的引用计数为 0 时,表示该对象不再被引用,可以被回收。
- 可达性分析算法:从根对象(如方法区中的类静态属性、方法中的局部变量等)出发,通过对象之间的引用链进行遍历,如果存在一条引用链到达某个对象,则说明该对象是可达的,反之不可达,不可达的对象将被回收。
- 终结器(Finalizer):如果对象重写了
finalize()方法,垃圾回收器会在回收该对象之前调用finalize()方法,对象可以在finalize()方法中进行一些清理操作。然而,终结器机制的使用不被推荐,因为它的执行时间是不确定的,可能会导致不可预测的性能问题。
如何获取私有类
- 公共方法访问器
- 反射机制
反射机制允许在运行时检查和修改类、方法、字段等信息,通过反射可以绕过
private访问修饰符的限制来获取私有对象
反射
动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制
反射特点
- 运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等。
- 动态对象创建:可以使用反射 API 动态地创建对象实例,即使在编译时不知道具体的类名。这是通过 Class 类的 newInstance() 方法或 Constructor 对象的 newInstance() 方法实现的。
- 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过 Method 类的 invoke() 方法实现,允许你传入对象实例和参数值来执行方法。
- 访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过 Field 类的 get() 和 set() 方法完成的。 反射与普通 new 对象区别:运行时才可以确定对象值,无法在编码的时候确定
反射场景
- 加载数据库驱动
- 配置文件加载
注解
原理:是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。我们通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。
public @interface MyAnnotation {
String value();
}
编译后,Java 编译器会将其转换为一个继承自 Annotation 的接口,并生成相应的字节码文件。
根据注解的作用范围,Java 注解可以分为以下几种类型:
- 源码级别注解 :仅存在于源码中,编译后不会保留(
@Retention(RetentionPolicy.SOURCE))。 - 类文件级别注解 :保留在
.class文件中,但运行时不可见(@Retention(RetentionPolicy.CLASS))。 - 运行时注解 :保留在
.class文件中,并且可以通过反射在运行时访问(@Retention(RetentionPolicy.RUNTIME))。 只有运行时注解可以通过反射机制进行解析。 当注解被标记为RUNTIME时,Java 编译器会在生成的.class文件中保存注解信息。这些信息存储在字节码的属性表(Attribute Table)中,具体包括以下内容: - RuntimeVisibleAnnotations :存储运行时可见的注解信息。
- RuntimeInvisibleAnnotations :存储运行时不可见的注解信息。
- RuntimeVisibleParameterAnnotations 和 RuntimeInvisibleParameterAnnotations :存储方法参数上的注解信息。
通过工具(如
javap -v)可以查看.class文件中的注解信息。 注解的解析主要依赖于 Java 的反射机制。以下是解析注解的基本流程:
- 获取注册信息:通过反射 API 可以获取类、方法、字段等元素上的注解。例如:
Class<?> clazz = MyClass.class;
MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
if (annotation != null) {
System.out.println(annotation.value());
}
- 底层原理:反射机制的核心类是
java.lang.reflect.AnnotatedElement,它是所有可以被注解修饰的元素(如Class、Method、Field等)的父接口。该接口提供了以下方法:getAnnotation(Class<T> annotationClass):获取指定类型的注解。getAnnotations():获取所有注解。isAnnotationPresent(Class<? extends Annotation> annotationClass):判断是否包含指定注解。 这些方法的底层实现依赖于 JVM 提供的本地方法(Native Method),例如:native Annotation[] getDeclaredAnnotations0(boolean publicOnly);native <A extends Annotation> A getAnnotation(Class<A> annotationClass);JVM 在加载类时会解析.class文件中的注解信息,并将其存储在内存中,供反射机制使用。 因此,注解解析的底层实现主要依赖于 Java 的反射机制和字节码文件的存储。通过@Retention元注解可以控制注解的保留策略,当使用RetentionPolicy.RUNTIME时,可以在运行时通过反射 API 来解析注解信息。在 JVM 层面,会从字节码文件中读取注解信息,并创建注解的代理对象来获取注解的属性值。
异常

- Error(错误):表示运行时环境的错误。错误是程序无法处理的严重问题,如系统崩溃、虚拟机错误、动态链接失败等。通常,程序不应该尝试捕获这类错误。例如,OutOfMemoryError、StackOverflowError 等。
- Exception(异常):表示程序本身可以处理的异常条件。异常分为两大类:
- 非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。
- 运行时异常:这类异常包括运行时异常(RuntimeException)和错误(Error)。运行时异常由程序错误导致,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。运行时异常是不需要在编译时强制捕获或声明的。
try{return “a”} finally{return “b”}返回了什么
finally 块中的 return 语句会覆盖 try 块中的 return 返回,因此,该语句将返回 “b”。
异常处理
- try-catch 语句块
- throw 语句
- throws 关键字
- finally 块
Object
Object 方法
- equals 方法:比较地址。如需比较内容重写 equals 方法,需要重写 hashcode 方法
- notify 和 notifyAll 方法,它们都是用于多线程同步的,和 synchronized 配合使用,作用是唤醒等待当前对象锁的线程。notify 是随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,比如生产者消费者模式中,生产者生产完数据后调用 notifyAll,唤醒等待的消费者线程。wait 方法有三个重载,作用是让当前持有对象锁的线程释放锁并进入等待状态,直到被 notify notifyAll 唤醒或者等待时间到期。同样需要在 synchronized 同步块或方法中使用,否则会抛 IllegalMonitorStateException,比如:
synchronized (lockObj) {
while (条件不满足) {
lockObj.wait(1000); // 等待1秒,超时自动唤醒
}
// 执行业务逻辑
}
- finalize 方法,它是对象被垃圾回收器回收前会调用的方法,默认是空实现。但现在基本不推荐使用,因为它的执行时机不确定,可能很久才执行甚至不执行,而且可能导致对象复活,影响垃圾回收效率,Java9 之后已经标记为过时,替代方案是使用 try with resources 或者 PhantomReference 来处理资源释放。
== 与 equals 有什么区别?
- ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
- equals():比较的是两个字符串的内容,属于内容比较。
ashcode 和 equals 方法有什么关系?
- 一致性:如果两个对象使用
equals方法比较结果为true,那么它们的hashCode值必须相同。也就是说,如果obj1.equals(obj2)返回true,那么obj1.hashCode()必须等于obj2.hashCode()。 - 非一致性:如果两个对象的
hashCode值相同,它们使用equals方法比较的结果不一定为true。即obj1.hashCode() == obj2.hashCode()时,obj1.equals(obj2)可能为false,这种情况称为哈希冲突。
String、StringBuffer、StringBuilder 的区别和联系
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 不可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 是(因不可变) | 否 | 是(同步方法) |
| 性能 | 低(频繁修改时) | 高(单线程) | 中(多线程安全) |
| 适用场景 | 静态字符串 | 单线程动态字符串 | 多线程动态字符串 |
Java 新特性
| 特性名称 | 描述 | 示例或说明 |
|---|---|---|
| Lambda 表达式 | 简化匿名内部类,支持函数式编程 | (a, b) -> a + b 代替匿名类实现接口 |
| 函数式接口 | 仅含一个抽象方法的接口,可用 @FunctionalInterface 注解标记 | Runnable, Comparator, 或自定义接口 @FunctionalInterface interface MyFunc { void run(); } |
| Stream API | 提供链式操作处理集合数据,支持并行处理 | list.stream().filter(x -> x > 0).collect(Collectors.toList()) |
| Optional 类 | 封装可能为 null 的对象,减少空指针异常 | Optional.ofNullable(value).orElse("default") |
| 方法引用 | 简化 Lambda 表达式,直接引用现有方法 | System.out::println 等价于 x -> System.out.println(x) |
| 接口的默认方法与静态方法 | 接口可定义默认实现和静态方法,增强扩展性 | interface A { default void print() { System.out.println("默认方法"); } } |
| 并行数组排序 | 使用多线程加速数组排序 | Arrays.parallelSort(array) |
| 重复注解 | 允许同一位置多次使用相同注解 | @Repeatable 注解配合容器注解使用 |
| 类型注解 | 注解可应用于更多位置(如泛型、异常等) | List<@NonNull String> list |
| CompletableFuture | 增强异步编程能力,支持链式调用和组合操作 | CompletableFuture.supplyAsync(() -> "result").thenAccept(System.out::println) |
Lambda 表达式
(parameters) -> expression:当 Lambda 体只有一个表达式时使用,表达式的结果会作为返回值。(parameters) -> { statements; }:当 Lambda 体包含多条语句时,需要使用大括号将语句括起来,若有返回值则需要使用return语句。
序列化
序列化和反序列化让你自己实现你会怎么做?
Java 默认的序列化虽然实现方便,但却存在安全漏洞、不跨语言以及性能差等缺陷。
- 无法跨语言: Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
- 容易被攻击:Java 序列化是不安全的,我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。
- 序列化后的流太大:序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。 我会考虑用主流序列化框架,比如 FastJson、Protobuf 来替代 Java 序列化。 如果追求性能的话,Protobuf 序列化框架会比较合适,Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。
设计模式
volatile 和 sychronized 如何实现单例模式
public class SingleTon {
// volatile 关键字修饰变量 防止指令重排序
private static volatile SingleTon instance = null;
private SingleTon(){}
public static SingleTon getInstance(){
if(instance == null){
//同步代码块 只有在第一次获取对象的时候会执行到 ,第二次及以后访问时 instance变量均非null故不会往下执行了 直接返回啦
synchronized(SingleTon.class){
if(instance == null){
instance = new SingleTon();
}
}
}
return instance;
}
}
正确的双重检查锁定模式需要需要使用 volatile。volatile 主要包含两个功能。
- 保证可见性。使用 volatile 定义的变量,将会保证对所有线程的可见性。
- 禁止指令重排序优化。 由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性
代理模式和适配器模式有什么区别?
- 目的不同:代理模式主要关注控制对对象的访问,而适配器模式则用于接口转换,使不兼容的类能够一起工作。
- 结构不同:代理模式一般包含抽象主题、真实主题和代理三个角色,适配器模式包含目标接口、适配器和被适配者三个角色。
- 应用场景不同:代理模式常用于添加额外功能或控制对对象的访问,适配器模式常用于让不兼容的接口协同工作。
责任链模式使用场景是什么?
责任链模式的使用场景核心很明确,就是一个请求需要多个独立的处理逻辑来承接,同时不想让请求发起方和所有处理者产生强关联,还得让处理流程能灵活调整,简单说就是谁能处理就谁来接手,整个处理顺序和参与节点能按需改动。
比如实际开发里最常遇到的接口请求校验,用户调用我们的接口时,可能得先检查登录状态,再验证 token 是否有效,接着确认接口访问权限,最后还要限制请求频率,这些校验逻辑各自独立,而且不同接口需要的校验步骤不一样,比如登录接口只需要验证验证码,查询用户信息的接口得同时过登录和权限校验。要是不用责任链,就得在每个接口里写一堆 if-else 把这些校验串起来,后续想改某个校验规则,所有相关接口都得动,维护起来特别麻烦。
这时候用责任链就很合适,我们可以把每个校验逻辑封装成一个处理节点,先定义一个抽象的处理者类:
// 抽象处理者
abstract class Handler {
protected Handler next;
// 设置下一个处理节点
public void setNext(Handler next) {
this.next = next;
}
// 抽象处理方法
public abstract boolean handle(Request request);
}
然后每个校验逻辑都继承这个类,比如登录校验:
// 登录态校验节点
class LoginHandler extends Handler {
@Override
public boolean handle(Request request) {
if (request.isLogin()) {
System.out.println("登录态校验通过,交给下一个节点");
// 校验通过,交给下一个处理者
return next != null ? next.handle(request) : true;
} else {
System.out.println("未登录,直接返回失败");
// 校验不通过,终止链路
returnfalse;
}
}
}
再写个权限校验节点、频率限制节点,最后在使用的时候,根据接口需求动态组装链路:
// 组装链路:登录校验 -> 权限校验 -> 频率限制
Handler loginHandler = new LoginHandler();
Handler authHandler = new AuthHandler();
Handler rateLimitHandler = new RateLimitHandler();
loginHandler.setNext(authHandler);
authHandler.setNext(rateLimitHandler);
// 发起请求
Request request = new Request(true, "admin", 1); // 已登录、管理员权限、第1次请求
boolean result = loginHandler.handle(request);
这样一来,请求发起方只需要调用第一个节点,根本不用关心后面有多少校验步骤,也不用知道具体是哪个节点在处理;如果某个接口不需要频率限制,直接去掉 rateLimitHandler 的组装就行,不用修改任何校验节点的代码,流程调整起来特别灵活。
I/O
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 的 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 方法中实现按照分数排序和按照学号排序的逻辑。
public class Student implements Comparable<Student> {
private int id;
private int score;
// 构造方法和其他属性、方法省略
@Override
public int compareTo(Student other) {
if (this.score != other.score) {
return Integer.compare(other.score, this.score); // 按照分数降序排序
} else {
return Integer.compare(this.id, other.id); // 如果分数相同,则按照学号升序排序
}
}
}
然后在需要对学生列表进行排序的地方,使用 Collections.sort() 方法对学生列表进行排序即可:
List<Student> students = new ArrayList<>();
// 添加学生对象到列表中
Collections.sort(students);
Native 方法
native 方法是一种特殊类型的方法,它允许 Java 代码调用外部的本地代码,即用 C、C++ 或其他语言编写的代码。native 关键字是 Java 语言中的一种声明,用于标记一个方法的实现将在外部定义。
在 Java 类中,native 方法看起来与其他方法相似,只是其方法体由 native 关键字代替,没有实际的实现代码。例如:
public class NativeExample {
public native void nativeMethod();
}
要实现 native 方法,你需要完成以下步骤:
- 生成 JNI 头文件:使用 javah 工具从你的 Java 类生成 C/C++ 的头文件,这个头文件包含了所有 native 方法的原型。
- 编写本地代码:使用 C/C++ 编写本地方法的实现,并确保方法签名与生成的头文件中的原型匹配。
- 编译本地代码:将 C/C++ 代码编译成动态链接库(DLL,在 Windows 上),共享库(SO,在 Linux 上)
- 加载本地库:在 Java 程序中,使用 System.loadLibrary() 方法来加载你编译好的本地库,这样 JVM 就能找到并调用 native 方法的实现了。