公开笔记

java 基础

Java 基础涵盖核心知识点:数据类型、值传递、装箱拆箱与 Integer 缓存;面向对象三大特性、设计原则、抽象类与接口区别;final/static、深浅拷贝、泛型、反射注解、异常、Object 类;还包含 Java 新特性、序列化、设计模式、BIO/NIO/AIO 及代码实操案例。

发布于 更新于

概念

1 Java 基础_1764170392368|697x405

值传递和引用传递

  • 值传递:传递的是实际值的副本,适用于基本数据类型(如 intchar 等),修改方法内的参数副本,不会影响原变量的值
  • 引用传递:本质仍然是值传递,传递的是对象引用的副本 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 的反射机制。以下是解析注解的基本流程:
  1. 获取注册信息:通过反射 API 可以获取类、方法、字段等元素上的注解。例如:
Class<?> clazz = MyClass.class;
MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
if (annotation != null) {
    System.out.println(annotation.value());
}
  1. 底层原理:反射机制的核心类是 java.lang.reflect.AnnotatedElement,它是所有可以被注解修饰的元素(如 ClassMethodField 等)的父接口。该接口提供了以下方法:
    • 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 的区别和联系

特性StringStringBuilderStringBuffer
不可变性不可变可变可变
线程安全是(因不可变)是(同步方法)
性能低(频繁修改时)高(单线程)中(多线程安全)
适用场景静态字符串单线程动态字符串多线程动态字符串

Java 新特性

特性名称描述示例或说明
Lambda 表达式简化匿名内部类,支持函数式编程(a, b) -> a + b 代替匿名类实现接口
函数式接口仅含一个抽象方法的接口,可用 @FunctionalInterface 注解标记RunnableComparator, 或自定义接口 @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 方法,你需要完成以下步骤:

  1. 生成 JNI 头文件:使用 javah 工具从你的 Java 类生成 C/C++ 的头文件,这个头文件包含了所有 native 方法的原型。
  2. 编写本地代码:使用 C/C++ 编写本地方法的实现,并确保方法签名与生成的头文件中的原型匹配。
  3. 编译本地代码:将 C/C++ 代码编译成动态链接库(DLL,在 Windows 上),共享库(SO,在 Linux 上)
  4. 加载本地库:在 Java 程序中,使用 System.loadLibrary() 方法来加载你编译好的本地库,这样 JVM 就能找到并调用 native 方法的实现了。
← 返回 Notes

Contact

Contact Me

Leave a message here. The form sends directly from the browser to a form delivery service and then to my email.

Messages are delivered to lzx744008464@gmail.com.