• [技术干货] Java 基础——Scanner 类
    一、Scanner 类概述Scanner 类是 Java 中用于获取用户输入的一个实用类,它位于 java.util 包下。通过 Scanner 类,可以方便地从多种输入源(比如标准输入流,也就是键盘输入,或者文件等)读取不同类型的数据,例如整数、小数、字符串等,大大简化了输入操作相关的编程工作。二、Scanner 类的创建在使用 Scanner 类之前,需要先创建它的对象。如果要从标准输入(键盘)读取数据,创建示例代码如下:import java.util.Scanner; public class ScannerExample {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        // 后续可使用该scanner对象进行输入读取操作    }}AI生成项目这里通过 new Scanner(System.in) 创建了一个 Scanner 对象,System.in 表示标准输入流,意味着后续操作将从键盘获取输入内容。三、常用方法及读取不同类型数据1.读取整数使用 nextInt() 方法可以读取用户输入的整数,示例代码如下:import java.util.Scanner; public class ReadInt {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        System.out.println("请输入一个整数:");        int num = scanner.nextInt();//程序执行到这里就会停下来,等待键盘的输入。                                    //键盘如果没有输入,这里就会一直卡着            //直到用户输入了内容之后,敲回车,这行代码就执行结束了             //这样就完成了数据从控制台到内存         System.out.println("你输入的整数是: " + num);        scanner.close();    }}AI生成项目这里提示用户输入整数后,调用 nextInt() 方法获取输入并赋值给 int 类型的变量 num,最后输出展示读取到的整数内容。需要注意的是,在读取完成后,如果不再需要使用该 Scanner 对象,最好调用 scanner.close() 方法关闭它,以释放相关资源。注意:针对nextInt()方法来说,只能接收整数数字。输入其他的字符串会报错。2.读取浮点数(小数)若要读取浮点数,可以使用 nextFloat() 方法(读取单精度浮点数) 或者 nextDouble() 方法(读取双精度浮点数),示例代码如下:import java.util.Scanner; public class ReadFloat {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        System.out.println("请输入一个单精度浮点数:");        float fNum = scanner.nextFloat();        System.out.println("你输入的单精度浮点数是: " + fNum);         System.out.println("请输入一个双精度浮点数:");        double dNum = scanner.nextDouble();        System.out.println("你输入的双精度浮点数是: " + dNum);         scanner.close();    }}AI生成项目上述代码分别演示了读取单精度和双精度浮点数的过程,按照提示输入相应类型的小数,就能通过对应方法获取并输出展示。3.读取字符串读取字符串有两种常用方式,一种是 next() 方法,一种是 nextLine() 方法。(1).next() 方法: 它读取输入的下一个单词(以空格、制表符等空白字符作为分隔符)细节:从键盘上接收一个字符串,但是接收的是第一个空格之前的内容示例代码1:import java.util.Scanner; public class ReadStringNext {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        System.out.println("请输入一些单词(以空格分隔):");        String word = scanner.next();        System.out.println("你输入的单词是: " + word);        scanner.close();    }}AI生成项目输入多个单词时,它只会获取第一个单词并返回。示例代码2:import java.util.Scanner;public class scannerTest {    public static void main(String[] args) {        Scanner s=new Scanner(System.in);        String username=s.next();        System.out.println(username);    }}AI生成项目(2).nextLine() 方法:该方法读取输入的一整行内容,示例代码:import java.util.Scanner; public class ReadStringNextLine {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        System.out.println("请输入一行文字:");        String line = scanner.nextLine();        System.out.println("你输入的文字内容是: " + line);        scanner.close();    }}AI生成项目它会获取从当前位置到行尾的所有字符,更适合读取完整的语句等情况。注意:从键盘上接收一个字符串,但是接收的是第一个换行符\n之前的内容可能遇到的问题及注意事项1.输入不匹配异常如果用户输入的数据类型和期望读取的数据类型不一致,例如:期望读取整数,但用户输入了字母等非数字内容,会抛出 InputMismatchException 异常。所以在实际应用中,可能需要添加异常处理代码来让程序更健壮,示例如下:import java.util.InputMismatchException;import java.util.Scanner; public class ExceptionHandle {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        try {            System.out.println("请输入一个整数:");            int num = scanner.nextInt();            System.out.println("你输入的整数是: " + num);        } catch (InputMismatchException e) {            System.out.println("输入的数据类型不正确,请重新输入整数!");        } finally {            scanner.close();        }    }}AI生成项目这里使用 try-catch 语句块捕获可能出现的输入类型不匹配异常,并在 catch 块中给出相应提示,无论是否出现异常,最终都会在 finally 块中关闭 Scanner 对象。2.nextLine() 方法使用的坑由于nextInt()只读取整数,不读取后续的换行符,这会导致nextLine()在下一次调用时直接读取空字符串。解决方法是使用nextLine()获取整数后的换行符,避免空字符导致的跳过输入现象。import java.util.Scanner;public class scannerTest {    public static void main(String[] args) {        Scanner s=new Scanner(System.in);        String username=s.next(); //无论是next(),还是nextInt(),nextDouble()这几个方法接收的是第一个空格之前的内容                         //而对于 son依旧留在缓存中,其在缓存的格式为 son\r(回车符)        System.out.println(username);         String position=s.nextLine();//执行到这一行时,nextLine回去缓存读内容,而读到\r时就结束了                                     //即读取了 son                                     //所以这一行的运行,用户无法输入东西        System.out.println(position);         String name =s.nextLine();        System.out.println(name);    }}AI生成项目运行结果: 当在调用 nextInt() 或者其他读取基本类型的方法(如 nextFloat() 等)后紧接着调用 nextLine() 方法时,可能会出现问题。因为 nextInt() 等方法读取数据后,留下的换行符(回车键对应的字符)会被 nextLine() 当作输入内容读取,导致 nextLine() 似乎 “跳过” 了用户的输入。解决办法通常是在调用 nextLine() 之前,先额外调用一次 nextLine() 来消耗掉前面留下的换行符,示例如下:import java.util.Scanner; public class NextLineIssue {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        System.out.println("请输入一个整数:");        int num = scanner.nextInt();        scanner.nextLine(); // 消耗掉换行符         System.out.println("请输入一行文字:");        String line = scanner.nextLine();        System.out.println("你输入的文字内容是: " + line);         scanner.close();    }}AI生成项目总之,Scanner 类在 Java 中是很常用的用于处理输入的类,掌握好它的使用方法以及注意相关问题,能更好地实现具有交互性的 Java 程序。————————————————原文链接:https://blog.csdn.net/m0_73941339/article/details/144803973
  • [技术干货] javaSE泛型、反射与注解
    泛型、反射与注解是 JavaSE 中支撑 “灵活编程” 与 “框架设计” 的核心技术。泛型解决 “类型安全” 问题,反射实现 “运行时动态操作类”,注解提供 “代码标记与元数据” 能力 —— 三者结合构成了 Java 框架(如 Spring、MyBatis)的底层基础。本章节将系统讲解这三项技术的核心原理与实际应用。一、泛型(Generic):编译时的类型安全保障在泛型出现之前,集合(如List)默认存储Object类型,取出元素时需强制转换,容易出现ClassCastException(类型转换异常)。泛型通过 “编译时类型指定”,让集合只能存储特定类型元素,从源头避免类型错误。1.1 泛型的核心作用编译时类型检查:限制集合(或泛型类)只能存储指定类型元素,编译阶段就报错,而非运行时崩溃。避免强制转换:取出元素时无需手动转换(编译器自动确认类型)。代码复用:一套逻辑支持多种类型(如List<String>、List<Integer>共用List的实现)。1.2 泛型的基本用法1.2.1 泛型类与泛型接口泛型类 / 接口在定义时声明 “类型参数”(如<T>),使用时指定具体类型(如String)。泛型类示例:// 定义泛型类:声明类型参数T(Type的缩写,可自定义名称)class GenericBox<T> {    // 使用T作为类型(类似变量,代表一种类型)    private T value;     // T作为方法参数和返回值    public void setValue(T value) {        this.value = value;    }     public T getValue() {        return value;    }} public class GenericClassDemo {    public static void main(String[] args) {        // 使用时指定类型为String(只能存String)        GenericBox<String> stringBox = new GenericBox<>();        stringBox.setValue("Hello"); // 正确:存入String        // stringBox.setValue(123); // 编译错误:不能存Integer         // 取出时无需转换(自动为String类型)        String str = stringBox.getValue();        System.out.println(str); // 输出:Hello         // 指定类型为Integer        GenericBox<Integer> intBox = new GenericBox<>();        intBox.setValue(100);        Integer num = intBox.getValue(); // 无需转换        System.out.println(num); // 输出:100    }}AI生成项目泛型接口示例:// 定义泛型接口(支持多种类型的“生产者”)interface Producer<T> {    T produce();} // 实现泛型接口时指定具体类型(如String)class StringProducer implements Producer<String> {    @Override    public String produce() {        return "生产的字符串";    }} // 实现时保留泛型(让子类也成为泛型类)class NumberProducer<T extends Number> implements Producer<T> {    private T value;     public NumberProducer(T value) {        this.value = value;    }     @Override    public T produce() {        return value;    }} public class GenericInterfaceDemo {    public static void main(String[] args) {        Producer<String> strProducer = new StringProducer();        String str = strProducer.produce();        System.out.println(str); // 输出:生产的字符串         Producer<Integer> intProducer = new NumberProducer<>(100);        Integer num = intProducer.produce();        System.out.println(num); // 输出:100    }}AI生成项目1.2.2 泛型方法泛型方法在方法声明时独立声明类型参数(与类是否泛型无关),适用于 “单个方法需要支持多种类型” 的场景。class GenericMethodDemo {    // 定义泛型方法:<E>是方法的类型参数,声明在返回值前    public <E> void printArray(E[] array) {        for (E element : array) {            System.out.print(element + " ");        }        System.out.println();    }     // 泛型方法带返回值    public <E> E getFirstElement(E[] array) {        if (array != null && array.length > 0) {            return array[0];        }        return null;    }} public class TestGenericMethod {    public static void main(String[] args) {        GenericMethodDemo demo = new GenericMethodDemo();         // 调用时自动推断类型(无需显式指定)        String[] strArray = {"A", "B", "C"};        demo.printArray(strArray); // 输出:A B C          Integer[] intArray = {1, 2, 3};        demo.printArray(intArray); // 输出:1 2 3          // 获取第一个元素(自动返回对应类型)        String firstStr = demo.getFirstElement(strArray);        Integer firstInt = demo.getFirstElement(intArray);        System.out.println("字符串数组第一个元素:" + firstStr); // 输出:A        System.out.println("整数数组第一个元素:" + firstInt);   // 输出:1    }}AI生成项目泛型方法特点:类型参数声明在方法返回值前(如<E>),与类的泛型参数无关。调用时编译器自动推断类型(无需手动指定,如传入String[]则E自动为String)。1.2.3 类型通配符(Wildcard)当需要处理 “未知类型的泛型” 时(如方法参数需要接收任意泛型List),使用通配符?及限定符(extends、super)控制类型范围。通配符形式    含义    适用场景<?>    任意类型(无限制)    仅读取元素,不修改(如打印任意 List)<? extends T>    上限:只能是 T 或 T 的子类    读取(可获取 T 类型),不能添加(除 null)<? super T>    下限:只能是 T 或 T 的父类    添加(可添加 T 或子类),读取只能到 Object通配符示例:import java.util.ArrayList;import java.util.List; public class WildcardDemo {    // 1. 无限制通配符<?>:接收任意List,只能读,不能写(除null)    public static void printList(List<?> list) {        for (Object obj : list) { // 只能用Object接收            System.out.print(obj + " ");        }        System.out.println();        // list.add("A"); // 编译错误:无法确定类型,不能添加非null元素    }     // 2. 上限通配符<? extends Number>:只能接收Number或其子类(如Integer、Double)    public static double sum(List<? extends Number> list) {        double total = 0;        for (Number num : list) { // 可安全转为Number            total += num.doubleValue(); // 调用Number的方法        }        return total;    }     // 3. 下限通配符<? super Integer>:只能接收Integer或其父类(如Number、Object)    public static void addIntegers(List<? super Integer> list) {        list.add(10); // 可添加Integer(或其子类,如Integer本身)        list.add(20);        // Integer num = list.get(0); // 编译错误:只能用Object接收    }     public static void main(String[] args) {        // 测试<?>        List<String> strList = List.of("A", "B");        List<Integer> intList = List.of(1, 2);        printList(strList); // 输出:A B         printList(intList); // 输出:1 2          // 测试<? extends Number>        List<Integer> integerList = List.of(1, 2, 3);        List<Double> doubleList = List.of(1.5, 2.5);        System.out.println("整数和:" + sum(integerList)); // 输出:6.0        System.out.println("小数和:" + sum(doubleList));  // 输出:4.0         // 测试<? super Integer>        List<Number> numberList = new ArrayList<>();        addIntegers(numberList); // 向NumberList添加Integer        System.out.println("添加后:" + numberList); // 输出:[10, 20]    }}AI生成项目1.3 泛型的局限不能用基本类型:泛型参数只能是引用类型(如List<int>错误,需用List<Integer>)。运行时擦除:泛型信息在编译后被擦除(如List<String>和List<Integer>运行时都是List),无法通过instanceof判断泛型类型。静态方法不能用类的泛型参数:静态方法属于类,而泛型参数随对象变化,若需泛型需定义为泛型方法。二、反射(Reflection):运行时的类信息操作反射允许程序在运行时获取类的信息(如类名、属性、方法、构造器),并动态操作这些成分(如调用私有方法、修改私有属性)。这打破了 “编译时确定代码逻辑” 的限制,让程序更灵活(也是框架实现 “自动装配”“依赖注入” 的核心)。2.1 反射的核心作用运行时获取类信息:无需提前知道类名,就能获取类的属性、方法等元数据。动态创建对象:通过类信息动态实例化对象(如Class.newInstance())。动态调用方法:包括私有方法(通过反射可绕过访问权限)。动态操作属性:包括私有属性(可修改值)。2.2 反射的核心类反射的所有操作都基于java.lang.Class类(类对象),它是反射的 “入口”。核心类 / 接口    作用Class    类的元数据对象,代表一个类的信息Constructor    类的构造器对象,用于创建实例Method    类的方法对象,用于调用方法Field    类的属性对象,用于访问 / 修改属性值2.3 反射的基本操作2.3.1 获取 Class 对象(反射入口)获取Class对象有 3 种方式,根据场景选择:class Student {    private String name;    private int age;     public Student() {}     public Student(String name, int age) {        this.name = name;        this.age = age;    }     public void study() {        System.out.println(name + "正在学习");    }     private String getInfo() {        return "姓名:" + name + ",年龄:" + age;    }} public class GetClassDemo {    public static void main(String[] args) throws ClassNotFoundException {        // 方式1:通过对象.getClass()(已知对象)        Student student = new Student();        Class<?> clazz1 = student.getClass();        System.out.println("方式1:" + clazz1.getName()); // 输出:Student         // 方式2:通过类名.class(已知类名,编译时确定)        Class<?> clazz2 = Student.class;        System.out.println("方式2:" + clazz2.getName()); // 输出:Student         // 方式3:通过Class.forName("全类名")(仅知类名,运行时动态获取,最常用)        Class<?> clazz3 = Class.forName("Student"); // 全类名:包名+类名(此处默认无包)        System.out.println("方式3:" + clazz3.getName()); // 输出:Student         // 验证:同一个类的Class对象唯一        System.out.println(clazz1 == clazz2); // 输出:true        System.out.println(clazz1 == clazz3); // 输出:true    }}AI生成项目说明:一个类的Class对象在 JVM 中唯一,是类加载的产物(类加载时 JVM 自动创建Class对象)。2.3.2 反射创建对象(通过构造器)通过Class对象获取Constructor,再调用newInstance()创建实例(支持无参和有参构造)。import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException; public class ReflectNewInstance {    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {        // 1. 获取Class对象        Class<?> clazz = Class.forName("Student");         // 2. 方式1:调用无参构造(若类无无参构造,会抛异常)        Student student1 = (Student) clazz.newInstance(); // 已过时,推荐用Constructor        System.out.println("无参构造创建:" + student1);         // 3. 方式2:调用有参构造(更灵活,推荐)        // 获取有参构造器(参数为String和int)        Constructor<?> constructor = clazz.getConstructor(String.class, int.class);        // 传入参数创建实例        Student student2 = (Student) constructor.newInstance("张三", 18);        System.out.println("有参构造创建:" + student2);    }}AI生成项目2.3.3 反射调用方法(包括私有方法)通过Method对象调用方法,支持公有和私有方法(私有方法需先设置setAccessible(true)取消访问检查)。import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method; public class ReflectInvokeMethod {    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {        Class<?> clazz = Class.forName("Student");        Student student = (Student) clazz.getConstructor(String.class, int.class).newInstance("张三", 18);         // 1. 调用公有方法(study())        // 获取方法:参数1为方法名,参数2为参数类型(无参则不写)        Method studyMethod = clazz.getMethod("study");        // 调用方法:参数1为实例对象,参数2为方法参数(无参则不写)        studyMethod.invoke(student); // 输出:张三正在学习         // 2. 调用私有方法(getInfo())        // 获取私有方法需用getDeclaredMethod(getMethod只能获取公有)        Method getInfoMethod = clazz.getDeclaredMethod("getInfo");        // 取消访问检查(关键:私有方法必须设置,否则抛异常)        getInfoMethod.setAccessible(true);        // 调用私有方法        String info = (String) getInfoMethod.invoke(student);        System.out.println("私有方法返回:" + info); // 输出:姓名:张三,年龄:18    }}AI生成项目2.3.4 反射操作属性(包括私有属性)通过Field对象访问或修改属性,私有属性同样需要setAccessible(true)。import java.lang.reflect.Field; public class ReflectOperateField {    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {        Class<?> clazz = Class.forName("Student");        Student student = (Student) clazz.getConstructor().newInstance(); // 无参构造创建         // 1. 获取并修改私有属性name        Field nameField = clazz.getDeclaredField("name"); // 获取私有属性        nameField.setAccessible(true); // 取消访问检查        nameField.set(student, "李四"); // 设置属性值(参数1为实例,参数2为值)         // 2. 获取并修改私有属性age        Field ageField = clazz.getDeclaredField("age");        ageField.setAccessible(true);        ageField.set(student, 20);         // 验证修改结果(调用之前的私有方法getInfo())        Method getInfoMethod = clazz.getDeclaredMethod("getInfo");        getInfoMethod.setAccessible(true);        String info = (String) getInfoMethod.invoke(student);        System.out.println("修改后信息:" + info); // 输出:姓名:李四,年龄:20    }}AI生成项目2.4 反射的应用场景框架底层:Spring 的 IOC 容器通过反射创建对象并注入依赖;MyBatis 通过反射将数据库结果集映射为 Java 对象。注解解析:自定义注解需配合反射获取注解标记的类 / 方法,执行对应逻辑(如权限校验)。动态代理:AOP 的动态代理(如 JDK 代理)基于反射实现方法增强。工具类:如 JSON 序列化工具(Jackson、FastJSON)通过反射获取对象属性并转为 JSON。2.5 反射的优缺点优点:灵活性高,支持运行时动态操作,是框架的核心技术。缺点:性能损耗:反射操作绕开编译优化,性能比直接调用低(但框架中影响可接受)。破坏封装:可直接访问私有成员,可能导致代码逻辑混乱。可读性差:反射代码较繁琐,不如直接调用直观。三、注解(Annotation):代码的标记与元数据注解(Annotation)是 Java 5 引入的特性,本质是 “代码的标记”,可在类、方法、属性等元素上添加,用于携带 “元数据”(描述数据的数据)。注解本身不直接影响代码逻辑,但可通过反射解析注解,执行对应操作。3.1 注解的核心作用标记代码:如@Override标记方法重写,编译器会校验是否符合重写规则。携带元数据:如@Test标记测试方法,测试框架会自动执行标记的方法。简化配置:替代 XML 配置(如 Spring 的@Controller标记控制器类)。3.2 常用内置注解Java 内置了 3 个基本注解(定义在java.lang包),编译器会识别并处理:注解名称    作用    使用位置@Override    标记方法为重写父类的方法,编译器校验    方法@Deprecated    标记元素(类、方法等)已过时,编译器警告    类、方法、属性等@SuppressWarnings    抑制编译器警告(如未使用变量警告)    类、方法等内置注解示例:public class BuiltInAnnotationDemo {    // @Deprecated:标记方法已过时    @Deprecated    public void oldMethod() {        System.out.println("这是过时的方法");    }     // @Override:标记方法重写(若父类无此方法,编译报错)    @Override    public String toString() {        return "BuiltInAnnotationDemo对象";    }     // @SuppressWarnings:抑制“未使用变量”警告    @SuppressWarnings("unused")    public void test() {        int unusedVar = 10; // 若没有@SuppressWarnings,编译器会警告“变量未使用”        // 调用过时方法(编译器会警告,但可执行)        oldMethod();    }     public static void main(String[] args) {        new BuiltInAnnotationDemo().test();    }}AI生成项目3.3 元注解:定义注解的注解自定义注解时,需要用 “元注解”(注解的注解)指定注解的 “作用范围”“保留策略” 等。Java 提供 4 个元注解(定义在java.lang.annotation包):元注解名称    作用    常用值@Target    指定注解可使用的位置(如方法、类)    ElementType.METHOD(方法)、ElementType.TYPE(类)等@Retention    指定注解的保留策略(生命周期)    RetentionPolicy.RUNTIME(运行时保留,可反射获取)@Documented    标记注解会被 javadoc 文档记录    无参数@Inherited    标记注解可被子类继承    无参数核心元注解:@Target和@Retention是自定义注解必须的 ——@Target限制使用位置,@Retention(RetentionPolicy.RUNTIME)确保注解在运行时存在(才能被反射解析)。3.4 自定义注解及解析自定义注解需配合反射使用:先定义注解,再在代码中标记,最后通过反射解析注解并执行逻辑。示例:自定义权限校验注解import java.lang.annotation.*;import java.lang.reflect.Method; // 1. 定义自定义注解@Target(ElementType.METHOD) // 注解只能用在方法上@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可反射获取@interface RequirePermission {    // 注解属性(类似方法,可指定默认值)    String value(); // 权限名称(如"admin")} // 2. 使用注解标记方法class UserService {    // 标记需要"admin"权限才能执行    @RequirePermission("admin")    public void deleteUser() {        System.out.println("执行删除用户操作");    }     // 标记需要"user"权限才能执行    @RequirePermission("user")    public void queryUser() {        System.out.println("执行查询用户操作");    }} // 3. 通过反射解析注解,实现权限校验class PermissionChecker {    // 模拟当前用户拥有的权限    private String currentPermission = "admin";     // 执行方法前校验权限    public void executeWithCheck(Object obj, String methodName) throws Exception {        // 获取方法对象        Method method = obj.getClass().getMethod(methodName);        // 判断方法是否有@RequirePermission注解        if (method.isAnnotationPresent(RequirePermission.class)) {            // 获取注解对象            RequirePermission annotation = method.getAnnotation(RequirePermission.class);            // 获取注解的权限值            String requiredPerm = annotation.value();            // 校验权限            if (currentPermission.equals(requiredPerm)) {                method.invoke(obj); // 权限通过,执行方法            } else {                throw new RuntimeException("权限不足,需要:" + requiredPerm);            }        } else {            // 无注解,直接执行            method.invoke(obj);        }    }} // 测试public class CustomAnnotationDemo {    public static void main(String[] args) throws Exception {        UserService userService = new UserService();        PermissionChecker checker = new PermissionChecker();         checker.executeWithCheck(userService, "deleteUser"); // 输出:执行删除用户操作(权限足够)        checker.executeWithCheck(userService, "queryUser");  // 输出:执行查询用户操作(权限足够)    }}AI生成项目解析流程:定义注解:用@Target和@Retention指定使用位置和生命周期。使用注解:在目标方法上添加注解,设置属性值。反射解析:通过method.isAnnotationPresent()判断是否有注解,method.getAnnotation()获取注解对象,进而获取属性值并执行逻辑。四、三者关系与总结泛型:编译时保障类型安全,避免强制转换,是编写健壮代码的基础。反射:运行时动态操作类信息,突破编译时限制,是框架灵活性的核心。注解:通过标记携带元数据,配合反射实现 “标记 - 解析 - 执行” 的逻辑,是简化配置的关键。三者结合构成了 Java 高级编程的基础 —— 泛型保证类型安全,反射提供动态能力,注解简化元数据携带,共同支撑了 Spring、MyBatis 等主流框架的设计。————————————————原文链接:https://blog.csdn.net/m0_64198455/article/details/149465961
  • [技术干货] Java--数据类型加运算符
    Java 源代码文件,通常以 .java 扩展名结尾,包含了用 Java 编程语言编写的类定义和方法实现。为了将这些源代码转换成 JVM 可以理解和执行的格式,需要使用 Java 编译器( javac )进行编译。编译完成后,生成的 .class 文件包含了 JVM 可以理解的字节码。这些字节码文件可以在任何安装了 JVM 的系统上执行,这是 Java 语言“一次编写,到处运行”(Write Once, Run Anywhere,WORA)特性的基础。编译 javac运行java.java.class 字节码文件JVM数据类型数据类型基本数据类型引用数据类型整数浮点类型字符类型布尔类型数组 类 接口 字符串......数据类型关键字    内存占用 (字节)    范围byte    1    -128 到 127short    2    -32,768 到 32,767int    4    -2,147,483,648 到 2,147,483,647long    8    -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807float    4    单精度 32 位 IEEE 754double    8    双精度 64 位 IEEE 754char    2    U+0000 到 U+FFFFboolean    1    true 或 false包装类只是针对基本的数据类型 对应的类类型类型转换的要点​ 在 Java 编程语言中,当进行算术运算时,较小的数据类型(如short和byte)会被隐式提升(upcast)到较大的数据类型(如int)以避免数据丢失。这种隐式类型转换被称为类型提升(type promotion)。然而,在将计算结果赋值回较小的数据类型变量时,需要进行显式类型转换(explicit type casting),以确保数据类型的兼容性和防止潜在的数据丢失。隐式类型提升(Type Promotion)在 Java 中,当不同类型的操作数参与算术运算时,较小的数据类型会自动转换为较大的数据类型。例如,当byte或short类型的变量与int类型的变量进行运算时,byte或short类型的变量会被提升为int类型。这种提升是自动的,不需要程序员显式进行类型转换。显式类型转换(Type Casting)在将运算结果赋值回较小的数据类型变量时,必须进行显式类型转换。这是因为,如果没有显式转换,较大的数据类型(如int)可能包含较小的数据类型(如byte或short)无法表示的值。直接赋值会导致编译器报错,因为这种操作可能会导致数据丢失。例如,如果一个int类型的变量存储的值超出了byte类型变量的表示范围,直接赋值将导致编译错误。为了解决这个问题,可以使用显式类型转换,如下所示:byte b = 10;int i = 200;i = i + b; // 自动类型提升:b 被提升为 int 类型b = (byte) i; // 显式类型转换:将 i 转换为 byte 类型AI生成项目java运行在这个例子中,b和i相加时,b被隐式提升为int类型。但是,当尝试将结果赋值回b时,必须显式地将i转换为byte类型,以确保结果在byte类型的范围内。数据丢失和类型安全(Data Loss and Type Safety)在进行显式类型转换时,必须小心处理可能的数据丢失。如果转换后的值超出了目标类型的表示范围,可能会导致数据丢失或不可预测的行为。因此,程序员有责任确保类型转换是安全的,并且不会导致数据丢失。编译器错误(Compiler Errors)如果不进行适当的类型转换,编译器会报错,因为 Java 是一种静态类型语言(statically-typed language),它要求在编译时就确定所有变量和表达式的类型。这种类型检查机制有助于在编译时捕获潜在的错误,从而提高程序的可靠性和稳定性。位运算和逻辑运算在Java中,位运算和逻辑运算是两种不同的运算类型,它们在底层对数据进行操作。以下是Java中支持的位运算符和逻辑运算符,以及如何使用它们的方法:逻辑运算符逻辑运算符用于执行逻辑比较,并返回布尔值(true或false)。• &&(逻辑与):当且仅当两个操作数都为true时,结果才为true。• ||(逻辑或):如果两个操作数中至少有一个为true,则结果为true。• !(逻辑非):反转操作数的布尔值。示例方法:位运算符位运算符对整数的二进制位进行操作。(黄色为简易口诀)• &(位与):对两个操作数的每一位执行逻辑与操作。有0则0,同1为1• |(位或):对两个操作数的每一位执行逻辑或操作。有1则1,同0为0• ^(位异或):对两个操作数的每一位执行逻辑异或操作。相同为0,不同为1• ~(位非):反转操作数的所有位。• <<(左移):将操作数的位向左移动指定的位数。• >>(右移):将操作数的位向右移动指定的位数。• >>>(无符号右移):将操作数的位向右移动指定的位数,左边用0填充。示例方法:public class BitwiseOperations {    public static int bitwiseAnd(int a, int b) {        return a & b;    }    public static int bitwiseOr(int a, int b) {        return a | b;    }    public static int bitwiseXor(int a, int b) {        return a ^ b;    }    public static int bitwiseNot(int a) {        return ~a;    }    public static int leftShift(int a, int shift) {        return a << shift;    }    public static int rightShift(int a, int shift) {        return a >> shift;    }    public static int unsignedRightShift(int a, int shift) {        return a >>> shift;    }    public static void main(String[] args) {        System.out.println(bitwiseAnd(10, 7));  // 输出 2        System.out.println(bitwiseOr(10, 7));   // 输出 15        System.out.println(bitwiseXor(10, 7));  // 输出 13        System.out.println(bitwiseNot(10));     // 输出 -11        System.out.println(leftShift(10, 2));   // 输出 40        System.out.println(rightShift(10, 2));  // 输出 2        System.out.println(unsignedRightShift(-10, 2)); // 输出 1073741822    }}AI生成项目java运行在这些示例方法中,我们定义了各种位运算和逻辑运算,并在main方法中提供了测试用例。您可以根据需要调用这些方法来执行相应的运算。请注意,位运算符只能用于整数类型(byte、short、int、long),而逻辑运算符只能用于布尔类型(boolean)。没有 <<< (无符号符号左移[不存在]) 因为有无符号都是用右边用0来补充,<<<会增加复杂度,带来不必要的麻烦,故不存在。最后两个注意点最后要注意的两个点是三目运算符和String字符的拼接:1.三目运算符所产生的值需要接收,否则编译器会报错2.String拼接的时候的顺序是从左到右,32+12 +"HelloWorld" //结果是"44HelloWorld"AI生成项目1"HelloWorld"+32+12 //结果是"HelloWorld44"————————————————原文链接:https://blog.csdn.net/2501_92963994/article/details/150390125
  • [技术干货] 【JAVA基础】由C++快速上手Java
    一、Java概述        这些笔者认为是快速上手java的核心知识,是java和c++的核心区别。有了这些知识能很快理解java的各种设计。1、JDK使用java首先要下载的就是jdk(Java Development Kit),用于构建和运行Java应用程序的完整开发环境。jdk里面包括:JVM:虚拟机,java程序实际上的运行环境,也是java“一次编译,处处运行”的关键。核心类库:Java官方提供的标准库,包含了大量预定义的类和接口,供开发者调用。开发工具:javac(将Java源程序.java编译成字节码文件.class)、java(Java解释器,用于在JVM里运行编译后的Java程序。)“一次编译,处处运行”        Java的一个核心特性,也是Java使用广泛的原因。由于java程序在JVM上运行,所以java程序可以在任何安装了有合适的JVM的平台上运行。2、Java是一门纯粹的面向对象的语言。2.1 mian()方法写在类中        与C++不同的是,c++程序的入口main函数可以作为全局的函数,不写在类中的,但是Java中所有函数(java里面称为“方法”)包括main方法都是写在一个类中的。使用intellJ IDEA运行某个类的时候,实际就是运行这个类里面定义的main方法;如果这个类没有定义main方法,则不能将这个类作为程序的入口点,这个类只能被其他代码调用。2.2 保存类信息的Class类        对于用户自定义的各种类和java官方提供的各种类,这些类的信息都是保存在一个Class类的实例对象中的。类的信息包括但不限于 类名、成员变量、构造器(即C++中的构造函数)、成员方法……        也就是说,java官方提供了一个类,类名就是Class。程序执行的时候,对于用户自定义的每一个类,JVM将会Class实例,将这些类的信息保存到这个Class的实例中。        类似的,对于类里面的构造器,java官方也提供了一个Constructor类,对于每个构造器,会使用一个Constructor实例来保存这个构造器的信息(如这个构造器是否私有)。3、Java的数据类型java中所有类型分为两大类:基本数据类型(类型后面的数字代表该类型几字节)整型(byte 1、short  2、int  4、long  8)、浮点型(float  4、double  8)字符型(char  2)布尔型(boolean :true / false)注意:java中的char是无符号的;布尔类型和整型之间不能互相显示/隐式转换。关于boolean的大小,Java规范并没有明确规定它占用多少字节,通常认为它是1位(bit),但实际上在内存中可能是1字节或更多,具体取决于JVM实现。引用数据类型其余任何类型。包括但不限于类、字符串String(String的本质也是一个类)、数组当一个变量存储的是一个引用类型数据的时候,该变量保存的实则是该数据在内存中的“地址”而非其本身。所有引用类型都是Object类的后代。        注意:Java没有指针的概念4、数据的在内存中的存储每个运行的java程序的内存分为三大块:栈(局部变量)堆(引用数据类型存储的位置:eg类的对象实例、数组)方法区(已被虚拟机加载的类信息、常量、静态变量) 一个栗子public class A {    int value;    void fun() {...}    static void sfun() {...}} public class Main {    public static void main(String[] args) {        A a = new A(); // step 1:创建A的实例a        a.fun();       // step 2:调用实例方法fun()        A.sfun();      // step 3:调用静态方法sfun()    }}AI生成项目java运行当Main.main方法开始执行时,会在栈中创建一个新的栈帧,用于存储局部变量a。a是一个引用类型的变量,它存储的是指向堆中A实例的引用(即对象的内存地址)。new A() 创建了一个A的实例,并将其存储在堆中 。这个实例包含了成员变量value、A类在方法区中的位置。类A的信息(包括类名、成员变量、构造器、成员方法等)存储在方法区中。调用a.fun()的过程首先在栈中找到局部变量a,获取其值(即指向堆中A实例的引用)。根据a的值找到堆中对应的A实例。在堆中找到A实例对应的类信息(位于方法区),从而找到fun()方法的相关信息。将fun()方法的相关信息加载到栈上,并执行该方法。调用a.sfun()的过程直接在方法区中找到类A的静态方法sfun()。加载sfun()方法的相关信息到栈上,并执行该方法。二、C++到Java快速上手语法1、Java的项目结构初学者主要了解以下项目结构。初学者写的Hello,World就是新建一个class文件写的。projectmodel(一般开发一个单独的功能,比如微信的朋友圈模块)package(用于组织类文件,对于不同包,在后面类的访问性修饰符有影响)class(如果一个类文件里面只有一个类,这个文件名需要和类名保持完全一致,包括大小写)2、注释        相同点:  // 单行注释     /* 多行注释 */        不同点:文档注释(笔者认为对于初学者来说不是重点,不会影响后续学习。笔者对此的了解也甚是浅薄)        Java 提供了一种专门用于生成文档的注释格式。有了这种注释格式,可以自动为编写的代码生成文档,这些文档通常以html文件的形式呈现。非常适合一些大型项目或者多人合作项目,方便查阅类和方法的作用。        文档注释以 /** 开始,并以 */ 结束。与普通注释不同,文档注释通常放置在类、接口、方法和字段声明之前。为了更好地描述代码,文档注释中可以使用一系列预定义的标签(tags),如“@author”。下面我将以一个栗子简单说明文档注释的用法:假如我有以下文件Example.java。//有“树树:”代表这是笔者写的文档注释,并没有实际含义 /** * 树树:这是一个简单的类,用于演示文档注释。 * * @author 张三 * @version 1.0 * @since 1.0 */public class Example {    /**     * 树树:一个简单的实例变量。     */    private int value;     /**     * 树树:构造函数,初始化实例变量。     *     * @param initialValue 初始值     * @throws IllegalArgumentException 如果初始值小于0     */    public Example(int initialValue) throws IllegalArgumentException {        if (initialValue < 0) {            throw new IllegalArgumentException("Initial value cannot be negative");        }        this.value = initialValue;    }     /**     * 树树:获取实例变量的值。     *     * @return 实例变量的值     * @see #setValue(int)     */    public int getValue() {        return value;    }     /**     * 树树:设置实例变量的值。     *     * @param newValue 新值     * @deprecated 使用 {@link #setValue(int)} 代替     */    @Deprecated    public void setValue(int newValue) {        this.value = newValue;    }     /**     * 树树:设置实例变量的新值。     *     * @param newValue 新值     */    public void setValueNew(int newValue) {        this.value = newValue;    }}AI生成项目java运行        使用命令为这个文件的文档注释生成文档(实则命令可以更复杂,为文档指定生成位置等,具体方法可以ai一下(。◕‿◕。)javadoc Example.javaAI生成项目bash        然后就能在对应文档生成位置找到文档。其实生成的文档是静态html网页资源         打开allclasses-index.html可以看见所有类的信息;点开index-all.html可以看见所有文档注释的内容。网页上方还提供了一个搜索框以便快速定位信息。 3、运算符大部分Java运算符的用法和功能都和C++一致,这里只说不同点。>>>C++没有该运算符。在 Java 中,>>> 是一个无符号右移运算符。它用于将一个整数的所有位向右移动指定的位数,并且在左侧填充0,无论该整数的最高位是0还是1。public class Main {    public static void main(String[] args) {        int signed = -8; // 11111111 11111111 11111111 11111000        int unsignedRightShift = signed >>> 2; // 00111111 11111111 11111111 11111110        System.out.println(unsignedRightShift); // 输出 1073741822         int signedRightShift = signed >> 2; // 11111111 11111111 11111111 11111110        System.out.println(signedRightShift); // 输出 -2    }}AI生成项目java运行Java不支持运算符重载4、分支结构switch1、Java支持的数据类型:byte、short、int、char、枚举(JDK5开始)、String(JDK7开始)2、case的值只能是字面量或者是编译时常量,而非表达式3、增强switch(JDK12开始),允许一个case匹配多个标签;引入符号“->”,使用该符号,每个case后面的代码块不用“break”也不会fall-through。int dayOfWeek = 2;switch (dayOfWeek) {    case 1, 2, 3 -> System.out.println("Weekday");    case 6, 7 -> System.out.println("Weekend");    default -> System.out.println("Invalid day");}AI生成项目java运行try-catch-finall        和c++相比,java多了一个finally块。不论try块里面的程序有没有成功执行,finally块里面的代码都最总会执行。(当然不要finally块,只要try-catch也是可以的。)try {    // 可能抛出异常的代码} catch (Exception e) {    // 处理异常} finally {    // 总是执行的代码}AI生成项目java运行5、数组一维数组        数组声明:如果只是声明数组变量,但是没有初始化,那此时这个数组变量的初始值是null,因为它没有指向任何对象。此时在内存中并没有为这个数组分配空间。int[] intArray; // 声明一个整数数组,此时intArray为nullAI生成项目java运行       数组初始化:必须指定数组内每个元素的值或者显示分配空间(new)。int[] arr={1,2,3};//初始化方式一:直接指定数组内每个元素的值int[] arr=new int[5]; //初始化方式二:先不指定数组内每个元素的值,但是分配了内存AI生成项目java运行        边界检查:Java中如果越界访问元素会抛出异常        length属性:数组本质上也是一个类,继承自Object。初始化一个数组就是创建了一个数组实例;声明一个数组就是创建一个“指向在堆中的数组实例”的引用变量。因此每个数组对象将会有Java提供的方法和属性。其中最常用的是length属性,用于获取数组的长度。int[] intArray = {1, 2, 3, 4, 5};System.out.println("Length of intArray: " + intArray.length); // 输出: 5AI生成项目java运行二维数组       普通二维数组的声明、使用和C++类似。//二维数组声明int[][] arrayName1; //方法一int arrayName2[][]; //方法二 //二维数组初始化//静态初始化int[][] arrayName3 = {    {1, 2, ..., n},    {1, 2, ..., n},    // ...};//动态初始化int[][] arrayName4 = new int[3][4]; //使用int[0][0]=1;AI生成项目java运行        锯齿数组        Java中的二维数组本质是数组的数组,而每个数组都是一个引用对象。也就是说,二维数组本质上是一个一维数组,只不过这个数组里面的每个元素都是一个一维数组的地址(即一维数组的引用对象)。这意味着,和C++不同的是,Java中的二维数组不必每一行的元素都一样多。栗子//方法一:int[][] jaggedArray = new int[3][]; // 只指定行数jaggedArray[0] = new int[]{1, 2, 3}; // 第一行有3个元素jaggedArray[1] = new int[]{4, 5}; // 第二行有2个元素jaggedArray[2] = new int[]{6, 7, 8, 9}; // 第三行有4个元素 System.out.println("Length of first row: " + jaggedArray[0].length); // 输出: 3System.out.println("Length of second row: " + jaggedArray[1].length); // 输出: 2System.out.println("Length of third row: " + jaggedArray[2].length); // 输出: 4 //方法二:int[][] jaggedArray2 = {    {1, 2},    {1, 2, 3,4}};AI生成项目java运行三、Java的面对对象类构造器        和C++一样,Java中每个类都有一个构造器(C++里称为构造函数)用于创建该类的实例。如果在自定义的类里面没有声明构造器,Java编译器将会自动生成一个无参构造器;如果有声明其他构造器(不论是有参还是无参),Java编译器将不会自动生成编译器。这意味着如果你同时需要无参构造器和有参构造器,你需要手动实现这两种构造器。        C++有但是Java不支持的:复制构造函数   在进行引用变量之间的相互赋值的时候,并不会创建新的实例,它们只是进行引用赋值析构函数   Java 没有析构函数的概念。Java 使用垃圾回收机制自动管理内存,当对象不再被引用时,垃圾回收器会在适当的时候回收其占用的内存。初始化列表  this关键字        C++中的this是一个指针,Java中this是一个引用变量,指向对象本身,它们的作用相似。可以使用this访问当前对象的属性或者方法,或者通过this()访问当前对象的构造器。栗子public class Rectangle {    private double width;    private double height;     //使用this访问属性    public Rectangle(double width, double height) {        this.width = width;            this.height = height;    }     // 使用this()访问构造器:    public Rectangle() {        this(1.0, 1.0);     }}AI生成项目java运行构造函数委托上面那个栗子中,在一个构造器中调用同一个类的另一个构造器,被称为构造函数委托。其中要注意的是this()调用其他构造器的代码需要放在第一行。static关键字修饰成员变量:与C++类似。该变量属于类而不是对象,所有对象共享同一个变量。可以通过类名或者对象名来访问该变量。修饰方法:与C++类似。该方法属于类而不是对象,所有对象共享。可以通过类名或者对象名来访问该方法。但是不能在静态方法中访问非静态成员。静态方法不能被override(重写),只能被隐藏。tip:工具类中的方法常常被声明为静态的,以便于使用工具类提供的功能。工具类的构造器通常被设计为私有的,这意味着理论上无法创建对应实例,因为工具类通常只是提供一个功能,创建实例对于工具类来说通常是不必要的。如Java官方提供的Math工具类提供各种数学运算方法:double result = Math.pow(2, 3); // 8.0AI生成项目java运行静态成员变量/方法 VS 非静态的成员变量/方法:访问前者只需要类名即可访问,而后者需要创建实例才能访问。静态代码块Java中的静态初始化块是C++中没有的,它在类加载时(注意不是创建对象时)执行,且只会执行一次。静态块通常用于复杂的静态变量初始化。public class MyClass {    static int counter;     static { // 静态初始化块        counter = 5;        System.out.println("Static block executed");    }}AI生成项目java运行类的五大成分成员变量、构造器、方法、代码块、内部类        前三成分C++中也有我们较为熟悉,再次只着重说明后两部分。代码块代码块分为静态代码块和实例代码块。静态代码块就是上文提到的,会在类加载的时候执行一次钱。实例代码块 不含static的代码块,包括在 {} 中,在创建对象的时候执行一次,且先于构造器执行。public class MyClass {    //实例代码块    {        System.out.println("Instance block executed");    }     public MyClass() {        System.out.println("Constructor executed");    }     public static void main(String[] args) {        new MyClass(); // 输出: Instance block executed, Constructor executed        new MyClass(); // 输出: Instance block executed, Constructor executed (每次创建对象时都会执行实例代码块)    }}AI生成项目java运行内部类定义在类内部的类。分为四种:成员内部类、静态内部类、局部内部类、匿名内部类成员内部类定义在外部类内的非静态类,属于外部类的对象所持有。它可以直接访问外部类的所有成员(包括私有成员),并且可以通过外部类的对象来创建实例。public class OuterClass {    private int outerField = 10;     // 成员内部类    public class InnerClass {        public void display() {            System.out.println("Outer field value: " + outerField); // 可以直接访问外部类的成员        }    }     public static void main(String[] args) {        OuterClass outer = new OuterClass();        OuterClass.InnerClass inner = outer.new InnerClass(); // 需要通过外部类对象创建内部类实例        inner.display(); // 输出: Outer field value: 10    }}AI生成项目java运行静态内部类也叫静态嵌套类,是使用 static 关键字修饰的内部类。它可以看作是外部类的一个静态成员,因此不能直接访问外部类的非静态成员(但可以访问静态成员);但可以通过类名来创建实例public class OuterClass {    private static int staticOuterField = 20;    private int nonStaticOuterField = 30;     // 静态嵌套类    public static class StaticNestedClass {        public void display() {            System.out.println("Static outer field value: " + staticOuterField); // 可以访问静态成员            // System.out.println(nonStaticOuterField); // 错误:无法访问非静态成员        }    }     public static void main(String[] args) {        // 创建静态嵌套类的实例        OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();        nested.display(); // 输出: Static outer field value: 20    }}AI生成项目java运行局部内部类定义在方法或代码块中的类。它只能在该方法或代码块的作用域内使用,并且可以访问外部类的所有成员以及方法中的局部变量(前提是这些局部变量必须是 final 或者实际上是不可变的)。public class OuterClass {    private int outerField = 40;     public void someMethod() {        final int localVar = 50; // 局部变量必须是 final 或者实际上是不可变的         // 局部内部类        class LocalInnerClass {            public void display() {                System.out.println("Outer field value: " + outerField);                System.out.println("Local variable value: " + localVar);            }        }         LocalInnerClass localInner = new LocalInnerClass();        localInner.display(); // 输出: Outer field value: 40, Local variable value: 50    }     public static void main(String[] args) {        OuterClass outer = new OuterClass();        outer.someMethod();    }}AI生成项目java运行匿名内部类匿名内部类是没有名字的内部类,通常用于实现接口或扩展抽象类。它们在定义时立即实例化,并且通常只使用一次。// 定义一个接口interface Printable {    void print();} public class OuterClass {     public static void main(String[] args) {        // 使用匿名内部类实现 Printable 接口        Printable printable = new Printable() {            @Override            public void print() {                System.out.println("Printing from anonymous inner class");            }        };         printable.print(); // 输出: Printing from anonymous inner class    }}AI生成项目java运行继承语法通过extends关键字实现继承java中的继承都是公有的不支持多重继承//继承public class Father{    private int value;    public void fun(){...}} public class Son extends Father{    //...}AI生成项目java运行通过关键字abstract来定义抽象类、抽象方法:和C++一样,Java中的类中如果有抽象方法(类似于C++的虚函数),该类就是抽象类。就算类中没有抽象方法,也可以使用abstract关键字将类声明为抽象类。抽象类不能被实例化,通常需要一个子类继承该抽象类并实现了所有抽象方法,该子类才能被实例化。public abstract class Animal {    // 抽象方法    public abstract void makeSound();     // 普通方法    public void sleep() {        System.out.println("The animal is sleeping.");    }}AI生成项目java运行权限修饰符修饰符    同一包内的类    子孙类(包括其他包)    所有类    说明private        default     默认情况下没有显式的修饰符。protected        public          Java中需要为每一个成员变量或者方法显示声明权限修饰符(除非使用默认权限default)方法重写@override 注解:写在方法的上一行,告诉编译器这是一个重写方法,编译器在编译阶段将会检查父类中是否有同名、同参的方法。子类重写父类方法时,重写的方法的访问权限应该≥父类对应方法;该方法的返回值的类型应该≤父类对应方法返回值。(返回值类型≤意思是该类型应该是父类方法对应返回值的类型或者其子孙类型)私有方法、静态方法不能被重写。重写toString()使用语句System.out.print(data)可以将变量输出到工作台上。对于基本数据类型和String,直接将变量打印到工作台上。对于引用数据类型,默认输出为类名@哈希码。其底层是调用Object的toString(),输出的是toString()方法的返回值。如果想自定义类的输出,应该重写类的toString()。栗子public class Person {    private String name;    private int age;     @Override    public String toString() {        return "Person{name='" + name + "', age=" + age + "}";    }     ...}AI生成项目java运行子类构造器调用父类构造器子类构造器在执行之前会先调用父类构造器。因此如果父类只有一个构造器,且这个构造器私有的话会报错,通常认为这个父类并不能被继承子类构造器在默认情况下会在代码第一行自动调用super()。但是如果父类没有无参构造器,子类构造器内的第一行要手动super(参数列表)class Parent {    public Parent(int value) {       // ...    }} class Child extends Parent {    public Child() {        super(10); // 必须显式调用父类的有参构造器        //...    }}AI生成项目java运行多态概述java中的多态强调对象多态、行为多态,但不强调成员变量多态。java中常常使用多态,即声明一个父类的引用类型数据,但是其指向的具体实现是子类。多态的好处:右边的真实对象和左边声明的引用类型是解耦合的。定义父类类型的形参,可以接受一切其子孙类型作为实参。使得代码的拓展性更强 class Animal {    public void makeSound() {        System.out.println("Animal makes a sound");    }} class Dog extends Animal {    @Override    public void makeSound() {        System.out.println("Dog barks");    }} class Cat : public Animal {public:    void makeSound() const override {        std::cout << "Cat meows" << std::endl;    }};  public class Main {    public static void main(String[] args) {        //在这个例子中,myAnimal 是 Animal 类型的引用,但它实际上指向的是 Dog 类型的对象。        //即使 Dog 类的具体实现发生变化,只要 makeSound() 方法的签名保持不变,        //main 方法中的代码不需要修改。        Animal myAnimal = new Dog(); // 父类引用指向子类对象        myAnimal.makeSound(); // 输出: Dog barks            //在这个例子中,makeAnimalSound 函数接受一个 Animal* 类型的参数,        //它可以接受任何 Animal 的派生类对象。        makeAnimalSound(new Dog()); // 输出: Dog barks        makeAnimalSound(new Cat()); // 输出: Cat meows    }}AI生成项目java运行注意:多态情况下声明的引用类型,不能调用对应子类实例独有的功能,只能调用声明类型有的功能。原因涉及多态的底层实现逻辑↓多态实现的底层逻辑方法表:JVM加载类的时候,为每一个类创建一个方法表,该表记录了该类每个方法的入口。子类会继承父类的方法表,如果子类Override了父类的某个方法,其方法表上对应的方法的入口也会重载。编译时类型检查:java源码在编译的时候会进行类型检查。可以认为,实现多态的时候,编译器只认一个引用变量声明时的类型而不在乎其具体引用对象的类型。它会检查这个引用变量调用的方法是否属于父类的方法,如果调用子类的独有方法编译器会认为类型不匹配而报错。动态绑定:程序运行的时候,在调用一个引用类型的方法的时候,实际上查找的时实例对应的类型的方法表。以此实现多态。多态下的类型转换向上转换(自动)  向上转型是安全的,因为子类对象一定是父类类型。Animal myAnimal = new Dog(); // 向上转型,Dog 对象被当作 Animal 类型AI生成项目java运行向下转换(手动 / 显示转换) 只有引用类型指向的对象确实是转换之后的类型才能转换,否则会抛出类型转换异常Animal myAnimal = new Dog();Dog myDog = (Dog) myAnimal; // 向下转型,将 Animal 引用转换为 Dog 类型//如果myAnimal指向的类型是Cat会在运行时抛出类型转换的异常。AI生成项目java运行final关键字该关键字可以理解为最后一次被定义。修饰类,该类不能被继承(工具类可以考虑使用final修饰)修饰方法,该方法不能被重写修饰变量,该变量仅能在声明时被赋值一次。如果修饰的是引用类型变量,该变量指向别的实例对象,但是指向的实例对象本身的内容时可以改变的。(类似C++的指针,指针指向的位置不变,但是指针指向位置上的内容是可以改变的)常量static finalJava中称被static final修饰的变量为常量。常量的标识符一般使用大写加下划线。(eg.  MAX_VALUE)Java中,编译时能确定值的常量将会在.class文件中被替换字面量(即内联)。因此将常用的配置信息定义成一个常量包,即便于管理又不会影响性能。抽象(abstract关键字)被abstract修饰的类是抽象类,不能被实例化,只有其非抽象的子孙类能实例化;被abstract修饰的方法是抽象方法,无需具体实现。抽象类中不一定有抽象方法;有抽象方法的类一定是抽象类。// 定义一个抽象类abstract class Animal {    // 抽象方法,没有方法体    abstract void makeSound();    // 普通方法    void sleep() {        System.out.println("This animal is sleeping.");    }}// 定义一个子类,继承抽象类class Dog extends Animal {    // 实现抽象方法    @Override    void makeSound() {        System.out.println("Woof! Woof!");    }}// 测试类public class Main {    public static void main(String[] args) {        // 创建子类对象        Dog myDog = new Dog();        // 调用实现的方法        myDog.makeSound(); // 输出: Woof! Woof!        // 调用继承的普通方法        myDog.sleep(); // 输出: This animal is sleeping.        // 以下代码会报错,因为抽象类不能被实例化        // Animal myAnimal = new Animal();    }}AI生成项目java运行*接口(interface)接口可以被类继承,且一个类可以继承多个接口,这弥补了Java中类与类之间不能多继承的缺陷。接口不能被实例化如果一个类继承了某个接口并且实现了该接口里定义的所有抽象方法,则称这个类为该接口的实现类;否则该类需要被设计成抽象类JDK8前的传统接口只能有常量成员变量和抽象方法。public interface Animal {    // 抽象方法:方法默认为公有抽象的;即自动加上关键字public abstract    void makeSound();     // 常量字段:成员变量默认为公有常量,且只能是常量    //即自动加上关键字 public final static    String KINGDOM = "Animalia";}AI生成项目java运行JDK8后的接口JDK8后的接口(新增三种方法)默认方法(需要加关键字 default):即普通方法,可以被实现类继承、重写。但是不能直接通过接口名调用;只能通过实现类或者其对象调用。私有方法(需要加关键字private):只能由接口中的其他实例方法(有方法体的方法)调用。静态方法(需要关键字static):默认为public的,属于接口的方法,可以直接通过接口名调用。实现类可以通过隐藏(Hide)的方式重载方法(类似C++的重载,需要参数列表不同)public interface Animal {    // 抽象方法:    void makeSound();     // 默认方法:带有默认实现的方法,可以被实现类继承或重写    default void sleep() {        System.out.println("Animal is sleeping");    }     // 私有方法:只能在接口内部调用,用于辅助其他方法    private void performAction(String action) {        System.out.println("Performing: " + action);    }     // 另一个默认方法,使用私有方法    default void eat() {        performAction("eating");    }     // 静态方法:属于接口本身,可以通过接口名直接调用    static void displayKingdom() {        System.out.println("Kingdom: Animalia");    }}AI生成项目java运行接口的使用继承接口的类称为该接口的实现类(使用关键字 implement),实现类必须要实现该接口的所有抽象方法;否则需要将该类定义为抽象类。实现类public class Dog implements Animal {    @Override    public void makeSound() {        System.out.println("Dog barks");    }     // 重写默认方法    @Override    public void sleep() {        System.out.println("Dog is sleeping");    }}AI生成项目java运行一个类实现多个接口,需要实现该接口定义的所有抽象方法(除非把这个类定义为抽象类)public interface Swimmable {    void swim();} public interface Flyable {    void fly();} public class Duck implements Swimmable, Flyable {    @Override    public void swim() {        System.out.println("Duck is swimming");    }     @Override    public void fly() {        System.out.println("Duck is flying");    }}AI生成项目java运行使用接口的实现类public class Main {    public static void main(String[] args) {        // 创建 Dog 对象并通过 Animal 接口引用        Animal myDog = new Dog();        myDog.makeSound(); // 输出: Dog barks        myDog.sleep();     // 输出: Dog is sleeping (重写了默认方法)        myDog.eat();       // 输出: Performing: eating (使用了私有方法)         // 创建 Cat 对象并通过 Animal 接口引用        Animal myCat = new Cat();        myCat.makeSound(); // 输出: Cat meows        myCat.sleep();     // 输出: Animal is sleeping (使用默认方法)        myCat.eat();       // 输出: Performing: eating (使用了私有方法)         // 调用静态方法        Animal.displayKingdom(); // 输出: Kingdom: Animalia    }}AI生成项目java运行该例子中使用Animal的引用类型来承接该接口的实现类实例,这种写法在Java中常用,尤其在使用Java提供的API时经常使用。使用接口的好处多态性:eg. myDog 和 myCat 都是 Animal 类型的引用,但它们分别指向了 Dog 和 Cat 的实例。在一个方法中接受 Animal 类型的参数,而无需关心传入的具体实现类是什么。public void animalSound(Animal animal) {    animal.makeSound();} // 调用animalSound(new Dog()); // 输出: Dog barksanimalSound(new Cat()); // 输出: Cat meowsAI生成项目java运行解耦合增强可扩展性强制实现规范提高代码复用性其他注意事项一个类继承了父类和接口,如果父类和接口中有同名方法(包括参数列表相同),优先使用父类。如果一个类继承了多个接口,这些接口中存在冲突的同名默认方法,需要重写该方法。四、注解这部分内容涉及反射机制。也就是说,注解的底层实现借助保存类信息的特殊的class类(前文提到)关于反射机制这篇文章没有提到,因为反射和注解都属于java高级的内容。初学者暂时跳过并无大碍。但是注解是java中十分重要的内容,专业的java开发人员必须掌握这部分内容。java中的特殊标记,如常见注解@Test、@Override。其作用是让其他程序根据注解信息来决定如何执行该程序。注解可以用在类上、构造器上、方法上、成员变量上、参数上等位置。 自定义注解注意:属性名后面要带括号!因为虽然它被称为属性,但在实现上,注解是官方接口Annotation的实现类。每次使用接口的时候都会创建该注解的类的一个实例。public @interface 注解名{    public 属性类型 属性名() default 默认值;}AI生成项目java运行自定义注解的使用方法下面通过一个例子来说明注解的使用方法:假如定义了以下注解://MyBook.javapublic @interface MyBook {    String name();    int age() default 18;    String[] address();}AI生成项目java运行则该注解的使用为//BookDemo.java@MyBook(name = "小王子",address = {"B612星球", "地球"})public class BookDemo {    //...}AI生成项目java运行属性age有默认值,因此使用该注解的时候可以不给age赋值。特殊属性value在 Java 注解里,value 是一个特殊的预定义属性名。如果注解里仅有一个value属性,那么在使用注解的时候可以不标明属性名。【例子】假如有以下注解public @interface Test {    String value(); // 唯一属性}AI生成项目java运行在使用该注解的时候可以省略属性名@Test("测试用例") // 最简洁的语法public void myTestMethod() {}AI生成项目java运行如果某个注解里除了value属性之外,其他属性独有默认值,在使用该注解的时候也可以使用简略语法。【例子】public @interface MyAnnotation {    String value(); // 无默认值,必须显式赋值    int id() default 0; // 有默认值    String[] tags() default {}; // 有默认值} // 正确使用:@MyAnnotation("简化语法") // 等价于 @MyAnnotation(value = "简化语法")public class MyClass {} // 也可以显式指定其他属性:@MyAnnotation(value = "完整语法", id = 123)public class MyClass2 {}AI生成项目java运行元注解用来注解注解的注解。官方提供了5中内置的元注解。以下两种是常用的元注解@Target:声明被修饰的注解只能在哪些地方使用@Retention:声明注解的保留周期【例子】下面是@Target的使用//MyAnnotation1.java@Target(ElementType.METHOD) // 限制注解仅能用于方法public @interface MyAnnotation1 {    // 注解定义} //MyAnnotation2.java//@Target注解接受的是一个列表,可以选择多个枚举类型@Target({ElementType.METHOD,ElementType.PARAMETER}) // 限制注解仅能用于方法和参数public @interface MyAnnotation2 {    // 注解定义}AI生成项目java运行注解的解析简单来说注解的解析就是分析一个类,或者类里面的一个方法,或者……上面有没有标注注解,并把注解里的信息拿出来分析的过程。既然要分析类的信息,势必要借助反射机制来实现。下面用一个例子说明注解的解析假设有如下自定义注解和自定义的类//MyBook.java//假如有以下自定义注解public @interface MyBook {    String name();    int age() default 18;    String[] address();} //BookInfo.java// 使用注解@MyBook(name = "小王子", address = {"B612星球", "地球"})class BookInfo {    // ...}AI生成项目java运行解析注解 ps:不了解反射机制的话,只需要简单理解成每一个类可以通过Class类(一个Java官方定义的类)获取这个类Class类的实例保存的就是自定义类的类信息。每一个自定义方法都可以通过一个Method类来获取这个方法的信息,每一个Method类的实例保存的就是这个自定义方法的信息。// AnnotationParser.javaclass AnnotationParser {    public static void main(String[] args) {        //通过class类获取BookInfo类的信息        Class<BookInfo> bookInfoClass = BookInfo.class;        //如果BookInfo类被MyBook注解        if (bookInfoClass.isAnnotationPresent(MyBook.class)) {            //将注解内容取出,做后续操作            MyBook myBookAnnotation = bookInfoClass.getAnnotation(MyBook.class);            System.out.println("书名: " + myBookAnnotation.name());            System.out.println("年龄: " + myBookAnnotation.age());            System.out.print("地址: ");            for (String address : myBookAnnotation.address()) {                System.out.print(address + " ");            }        }    }}AI生成项目java运行五 java常用apiCollection 集合集合体系:MapCollectionList 接口(元素有序、可重复、有索引)ArrayListLinkedListSet 接口(元素无序、不重复、无索引)TreeSetHashSetLinkedHashSet(有序不重复、无索引) 集合的遍历方法一:迭代器方法二:增强for方法三 :Lambdaaction是匿名内部类对象,forEach()将会对每个元素,调用action内唯一的抽象方法accept()。遍历时引发的并发删除异常增强for和Lambda内部都是使用迭代器实现。如果在遍历的时候删除会引发异常。解决方法:for下标遍历:正序遍历,删除之后i--倒序遍历迭代器遍历删除的时候用iterator.remove()迭代器remove()的特殊性迭代器的remove()方法是专为遍历中修改设计的,它会: 确保删除操作与迭代器的状态同步(更新expectedModCount);处理索引偏移问题(如删除元素后,后续元素的索引会前移,迭代器会正确跳过)。ListArrayList VS LinkedList由于LinkedList首尾操作极快,因此很适合用于设计queue或者stack。Set 集合HashSet扩容机制:当 元素个数>数组长度*加载因子 时,数组双倍扩容 java重写equal的时候也会重写hashcode为什么?如果两个对象通过equals()比较相等,那么它们的hashCode()必须返回相同的值。如果两个对象的hashCode()不同,它们一定不相等(但hashCode()相同不保证对象相等,可能存在哈希冲突)。LinkedHashSetTreeSet自定义排序规则 方法二:例子 Map 集合常用方法 Map 遍历方式一:键找值例子:map是上文定义好的一个HashMap集合。 Map 遍历方法二:键值对entrySet()方法,底层遍历每个键值对,并把每个键值对包装成一个entry对象,最后将这些对象放进一个set里面。Map 遍历方式三:LambdaBiConsumer是匿名内部类对象,forEach()将会对每个元素,调用BiConsumer内唯一的抽象方法accept()。forEach内部通过方法二遍历。不用Lambda表达式也可以这样写: HashMap 底层原理并发编程线程创建方法一:这种方式需要创建一个类继承Thread类,并且重写其run()方法。run()方法中的代码就是线程要执行的任务。之后创建该类的实例并调用start()方法,线程便会启动。注意必须是调用start()才是开启子线程,如果调用run()相当于还是单线程。class MyThread extends Thread {    // 重写run方法,定义线程执行的任务    @Override    public void run() {        System.out.println("继承Thread类创建的线程正在执行");    }} public class ThreadCreationExample {    public static void main(String[] args) {        // 创建线程实例        MyThread thread = new MyThread();        // 启动线程        thread.start();    }}AI生成项目java运行方法二:实现Runnable接口。class MyRunnable implements Runnable {    // 实现run方法,定义线程执行的任务    @Override    public void run() {        System.out.println("实现Runnable接口创建的线程正在执行");    }} public class RunnableExample {    public static void main(String[] args) {        // 创建Runnable实现类的实例        MyRunnable runnable = new MyRunnable();        // 将runnable实例传递给Thread类的构造函数        Thread thread = new Thread(runnable);        // 启动线程        thread.start();    }}AI生成项目java运行该方式可以使用 匿名内部类 / 函数式编程 来简化语句。public class AnonymousRunnableExample {    public static void main(String[] args) {        // 创建匿名内部类实现Runnable接口,并直接传递给Thread构造函数        Thread thread = new Thread(new Runnable() {            @Override            public void run() {                System.out.println("使用匿名内部类创建的线程正在执行");            }        });         //使用函数式编程        Thread thread = new Thread(()->{                System.out.println("使用函数式编程创建的线程正在执行");        });                // 启动线程        thread.start();    }}AI生成项目java运行方式三:实现callable接口并使用futureTask该接口是一个泛型接口,泛型的类型要和需要返回的返回值类型相同。import java.util.concurrent.*; class MyCallable implements Callable<String> {    // 实现call方法,定义线程执行的任务,有返回值    @Override    public String call() throws Exception {        return "实现Callable接口创建的线程执行完成";    }} public class CallableExample {    public static void main(String[] args) throws ExecutionException, InterruptedException {        // 创建Callable实现类的实例        MyCallable callable = new MyCallable();        // 使用FutureTask包装Callable对象        FutureTask<String> futureTask = new FutureTask<>(callable);        // 将futureTask传递给Thread类的构造函数        Thread thread = new Thread(futureTask);        // 启动线程        thread.start();        // 获取线程执行的返回结果        String result = futureTask.get();        System.out.println(result);    }}AI生成项目java运行仅仅方法三有返回值,因为前两种重写的run()方法固定没有返回值。使用futureTask的get()方法获取返回值的时候,如果此时线程还没有执行完成,主线程会让出CPU直到线程执行完毕。Thread 的常用方法线程安全的方式同步代码块:synchronized{}把访问共享资源的核心代码上锁。synchronized(锁对象) {    // 需要同步的核心代码(访问共享资源的代码)}AI生成项目java运行锁对象:可以是任意 Java 对象(通常使用 this 或类的静态实例),用于标识哪个线程可以执行该代码块。如果两个线程获取的锁对象是相同的,这两个线程只有一个能够进入该同步代码块;如果两个线程获取的锁对象不同,这两个线程能够同时进入该同步代码块。作用:当一个线程进入同步代码块时,会先获取锁对象的锁,执行完代码后释放锁。其他线程必须等待锁被释放后才能获取锁并执行代码块。同步方法Lock 锁:ReentrantLock使用具体的锁对象来实现上锁解锁。 public class LockDemo {    private final ReentrantLock lock = new ReentrantLock();    private int sharedResource = 0;     public void fun() {        // 执行不涉及共享资源的功能(无需同步)                lock.lock();        try {            // 只对访问共享资源的代码加锁            sharedResource++;        } catch (Exception e) {            // 处理异常(例如记录日志)            e.printStackTrace();        } finally {            lock.unlock();        }                // 执行其他不涉及共享资源的功能(无需同步)    }}AI生成项目java运行线程池重复利用线程的技术。工作原理 方式一:通过ThreadPoolExecutor 创建runnable对象任务:import java.util.concurrent.*; public class ThreadPoolDemo {    public static void main(String[] args) {        // 1. 配置参数        int corePoolSize = 3; // 正式工3人        int maxPoolSize = 5;  // 最多招5人(3正式+2临时)        long keepAliveTime = 60; // 临时工空闲60秒就裁        TimeUnit unit = TimeUnit.SECONDS; // 时间单位:秒                // 任务队列:最多存10个任务(超出则触发拒绝策略)        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10);                 // 线程工厂:自定义线程名(方便调试)        ThreadFactory threadFactory = r -> new Thread(r, "MyPool-Thread-");                 // 拒绝策略:任务满了,让调用者自己执行(同步执行)        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();                 // 2. 创建线程池        ThreadPoolExecutor executor = new ThreadPoolExecutor(                corePoolSize,                 maxPoolSize,                 keepAliveTime,                 unit,                 workQueue,                 threadFactory,                 handler        );                // 3. 提交任务(模拟业务)        for (int i = 0; i < 20; i++) {            int taskId = i;            executor.execute(() -> {                System.out.println(                    Thread.currentThread().getName() + " 处理任务:" + taskId                );                // 模拟任务耗时                try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }            });        }                // 4. 关闭线程池(业务结束后调用)        executor.shutdown();    }}AI生成项目java运行创建callable对象任务:public class CallableExample {    public static void main(String[] args) {        // 创建固定大小的线程池        ExecutorService executor = Executors.newFixedThreadPool(3);                // 创建并提交多个Callable任务        Future<Integer> future1 = executor.submit(new FactorialTask(5));        Future<Integer> future2 = executor.submit(new FactorialTask(7));        Future<Integer> future3 = executor.submit(new FactorialTask(10));                // 获取任务结果        try {            System.out.println("5! = " + future1.get()); // 输出: 120            System.out.println("7! = " + future2.get()); // 输出: 5040            System.out.println("10! = " + future3.get()); // 输出: 3628800        } catch (InterruptedException | ExecutionException e) {            System.err.println("任务执行异常: " + e.getMessage());        } finally {            // 关闭线程池            executor.shutdown();        }    }} // 实现Callable接口的任务类(计算阶乘)class FactorialTask implements Callable<Integer> {    private final int number;        public FactorialTask(int number) {        this.number = number;    }        @Override    public Integer call() throws Exception {        /....        return result;    }}AI生成项目java运行 方式二:通过Executors工具类 其他lombok库(为类自动生成getter方法)这个库的主要功能是提供了一些很有用的注解。@Getter / @Setter:自动生成字段的 Getter 和 Setter 方法。@ToString:自动生成 toString() 方法。@EqualsAndHashCode:自动生成 equals() 和 hashCode() 方法。@Data:组合注解,包含 @Getter、@Setter、@ToString、@EqualsAndHashCode 等功能。@NoArgsConstructor:生成无参构造函数。@AllArgsConstructor:生成全参构造函数。@RequiredArgsConstructor:生成包含 final 字段或 @NonNull 字段的构造函数。Getter / Setter 方法在实际的开发项目中,通常需要为类中的每个成员变量提供共有的get方法(用于获取该成员变量的值)和公有的set方法(用于修改该成员变量的值),然后将这些成员变量设为私有的。这么做是为了隐藏实现细节:类的内部实现可以随时更改,而不会影响使用该类的代码。控制访问权限:通过 Getter 和 Setter 方法,可以控制字段的读写权限。数据验证:在 Setter 方法中可以添加逻辑,确保数据的有效性。虽然 Getter 和 Setter 方法很重要,但手动编写它们会增加代码量。Lombok 库通过注解自动生成这些方法,从而简化代码。栗子源文件:使用@Getter  @Setter注解import lombok.Getter;import lombok.Setter; @Getter@Setterpublic class User {    private String name;    private int age;}AI生成项目java运行编译成的.class文件:会自动生成每个成员变量的get方法和set方法public class User {    private String name;    private int age;     // Getter for name    public String getName() {        return this.name;    }    // Setter for name    public void setName(String name) {        this.name = name;    }    // Getter for age    public int getAge() {        return this.age;    }    // Setter for age    public void setAge(int age) {        this.age = age;    }————————————————原文链接:https://blog.csdn.net/2301_76391858/article/details/146043230
  • [技术干货] Java - WebSocket
    先启动服务端,再启动客户端三、PostMan调用3.1、Websocket在线模拟请求工具访问访问地址:http://www.jsons.cn/websocket/具有进行连接、断开、模拟发送数据等功能。(请求时注意连接格式为 ws://IP或域名:端口(示例 ws://127.0.0.1:8089/websocket/devices) 3.2、Postman使用新版的Postman1、建立 WebSocket 连接在 File–> New 页面,选择 WebSocket Request,即可创建一个 WebSocket 模拟请求。2、模拟数据交互在地址栏中输入相应的 WebSocket 请求地址,点击地址栏右侧的 “Connect” 按钮,即可建立连接。连接建立成功后,在 Message 的信息栏中输入模拟数据,点击 “Send” 按钮,即可与服务端进行数据交互。优势:支持多种数据格式如:Text、JSON、XML、HTML、Binary等;支持对交互信息进行格式化显示如:Text、JSON、XML、HTML等;支持对交互数据进行模糊搜索、筛选过滤、清空等操作;交互数据按照时间倒序显示,更便于查看最新的数据。3、断开 WebSocket 连接如果调试结束,点击地址栏右侧的 “Disconnect” 按钮,即可断开与 WebSocket 服务端的连接————————————————原文链接:https://blog.csdn.net/MinggeQingchun/article/details/142757957
  • [技术干货] JAVA调用dify工作流,获取工作流输出的文件
    最近在学习dify方面的知识,过程遇到的一些小困难,记录一下。java调用的代码主要参考这个基于 Spring Boot 和 Dify 实现流式响应输出_springboot dify-CSDN博客当然,你可能做点修改如果你直接部署dify,没有进行其他相关配置,那么在通过java调用后,应该会输出以下的内容流式结果:StreamResponse(event=message, id=44f74b98-22bf-43e6-916c-f738e5704d79, task_id=d05f7e19-640c-4292-bb2b-de1681e803cf, message_id=44f74b98-22bf-43e6-916c-f738e5704d79, answer=这是根据您需求生成的文件:[a0277b8185f64150bbecd372b7023c8a.xlsx](/files/tools/28f48078-8dbc-4954-aff2-ec8bc8fd7ded.xlsx?timestamp=1746515049&nonce=da4ed54481aad55f8c8d71afe5b784af&sign=VMiX1jCevCsyCQIt8dSfszA5jJCCX5An9dD3Z6wl8lE=), created_at=1746514907, conversation_id=f41acbdb-54b7-4318-82de-e7d7448928c5, data=null)主要看/files/tools/当我们在dify的页面下载文件时,也就是这时候其他访问的是127.0.0.1:80/files/tools/.....;所以能下载文件,dify默认启动端口就是80,,当用java代码调用时返回的只有/files/tools.....,然后前端点击时,会判断你这个地址正不正确,/files/tools...,没有http://或者https://,也没有端口,此时你点击访问的路径会是你前端的路径+/files/tools,例如http://localhost:1002/files/tools....,所以会访问不到,这时后只要我们修改一下。env文件就行了,找到你安装dify目录的docker目录,里面可以找到.env文件不同版本内容可能有所差异,我的是dify1.3,FILES_URL本来是空的,这里加上dify的路径就行,我是运行在80端口,所以我这里没加端口,如果你修改了运行的端口,这里记得要加上,修改完后在dify的docker目录下命令行执行docker-compose down && docker-compose up -d即可,如果你直接在这里停止又启动,大概率没效果,起码我是没有,所以用上面的命令启动,完成之后再次调用,此时返回的就是http://127.0.0.1/files/tools/......此时我们在自己的页面点击就能下载文件了。当然,获取方式有很多种,这只是其中一种而已。其他的就自行研究了————————————————原文链接:https://blog.csdn.net/qq_58983013/article/details/147740125
  • [技术干货] Java 的 Thread类必会小技巧,教你如何用多种方式快速创建线程
    前言想象一下,如果你的电脑只能一次执行一个任务,那会是多么的低效。幸运的是,Java提供了一种强大的机制,允许程序同时执行多个任务。这就是我们今天要探讨的主题——Java中的Thread类。目录Thread 类创建线程的方式线程终止一. Thread 类1. Thread 类的初识对于线程的概念, 本身是 操作系统内核提出的概念 。如果要执行并发编程, 就需要掌握不同的系统 api (例如 window 系统api , Linux 系统api ) 等… 这种 不同系统的api 是不一样的。对于我们 Java程序猿 来说, Java的api 早已分装好 对应的系统api , 我们这只需要学习 Java的api 即可。而进行并发编程的 Java最重要的api 就是 标准库中 Thread 类 。通过这个类, 我们可以实现对于 线程的创建 , 以及利用每个线程进行 业务逻辑和任务 的执行。鱼式疯言并且 Thread 是 java.lang 下面的一个库, 是 不需要手动导包 的。二. 创建线程的方式1. 创建线程<1>. 代码展示/** * 创建线程: * 继承 Thread * 重写 run 方法 */public class MyThread extends Thread{    @Override    public void run() {        while (true) {            System.out.println("MyThread的run线程 正在运行...");            try {                MyThread.sleep(10000);            } catch (InterruptedException e) {                throw new RuntimeException(e);            }        }    }    public static void main(String[] args) throws InterruptedException {        MyThread myThread =new MyThread();        // 创建一个线程        myThread.start();        // 主方法        while (true) {            System.out.println("main线程 正在运行...");            MyThread.sleep(10000);        }    }}AI生成项目java运行<2>. 创建流程首先创建一个先 继承Thread 类 , 并重写 run 方法在 main 方法 中 实例化一个 MyThread 类 。调用start() 方法来 创建线程 , 并执行run 方法里面的 业务逻辑。鱼式疯言补充总结:由于我们在执行程序时, 系统就会分配资源, 自动创建进程 , 并且程序是需要 调度执行 , 所以 main 方法自身就是一个线程: 主线程所以上述过程中, 可以认为 在 主线程 中又 创建了一个线程 。<3>. 逻辑分析run 中写着的是这个线程需要执行的 各种代码逻辑 , 相当于在 main方法 中的 代码同等含义。虽然在上述代码中 , 没有直接调用重写 的run 方法 , 但是当我们调用 start() 方法后, 在 Java代码中会重新调用我们重写的 run 方法 。如果只是单纯的调用 run 方法 , 并 不能创建线程 。对于上述过程有两个线程: 主线程 和 myThread , 都是分布在同一个系统资源下的, 所以需要 并发执行 : 两个死循环都 不会相互制约 , 各自执行各自的 。 抢占执行: 不能确定是哪个线程先执行到对应的逻辑。鱼式疯言补充细节 :上诉代码中, 我们用到了 sleep() 方法,这个方法是 Thread 中的静态方法 (类方法) 。 可以让程序 休眠一段时间 , 调用这个目的: 就是让程序猿好 观察程序的执行过程 。() 内参数 指定的是多少 毫秒(ms) , 用于 指定休眠多少时间 , 其中 1000 ms = 1s 换算进制。调用sleep() 和 创建线程的过程 , 都是需要 抛出: InterruptedException 这个异常的2. 创建线程的其他方式对于创建线程的方式:除了上述 继承 Thread 重写run方法 之外还有其他常见四种方式实现 Runnable 接口 , 重写 run 方法;使用 匿名内部类 , 对 Thread 类 重写 run 方法 ;使用 匿名内部类 , 实现 Runnable 接口 重写run 方法 ;使用 lambda 表达式 , 对 匿名内部类 进行简化 。<1>. 实现 Runnable 接口/** * 方法二: * 继承 Runnable * 实现 run 方法 * */class  MyThread1 implements  Runnable {    @Override    public void run() {        while (true) {            System.out.println("Runnable 中run线程 正在运行...");            try {                MyThread.sleep(1000);            } catch (InterruptedException e) {                throw new RuntimeException(e);            }        }    }    public static void main(String[] args) throws InterruptedException {        Thread myThread1 = new Thread(new MyThread1());        myThread1.start();        // 接口        while (true) {            System.out.println("main线程 正在运行");            MyThread.sleep(1000);        }    }}AI生成项目java运行对于这种方式的实现主要的流程还是在实例化Thread 对象之前 :首先 创建一个类 MyThread1 用于 实现 Runnable 接口重写 run 方法最终 new出对象 MyThread1 作为 Thread() 的参数 进行传入进行 实例化Thread 对象 即可。后面的过程就和上面相同了, 小编在这里就不赘述了 <2>. 匿名内部类—— 重写 Thread 的 run 方法/** * * 方法四: Thread 的匿名内部类 * 在匿名内部类中重写 Run 方法 * */class  MyThread3 {    public static void main(String[] args) throws InterruptedException {        Thread myThread3 = new Thread(){            @Override            public void run() {                while (true) {                    System.out.println("匿名类 MyThread的run线程 正在运行...");                    try {                        MyThread.sleep(1000);                    } catch (InterruptedException e) {                        throw new RuntimeException(e);                    }                }            }        };        // 创建线程        myThread3.start();        // 输出主线程        while (true) {            System.out.println("main 线程正在运行...");            MyThread.sleep(1000);        }    }}AI生成项目java运行我们知道, 对于 匿名内部类 来说, 是一种不带引用的一种对象, 也就是说当实例化 Thread 类对象 时, 我们使用匿名内部类的方式就是在 Thread () 后面 加上 { } , 并在{ } 内部 重写 run 方法 。以上就是唯一的区别 , 其他操作都一样。<3>. 匿名内部类 —— 实现 Runnable 接口的 run 方法 /*** 方法三 : Runnable 的匿名内部类* 在匿名内部类中 实现 Run 方法**/class  MyThread2 {   public static void main(String[] args) {       Thread myThread2 = new Thread(new Runnable() {           @Override           public void run() {               while (true) {                   System.out.println("内部类Runnable 中 Run方法正在执行...");                   try {                       MyThread.sleep(1000);                   } catch (InterruptedException e) {                       throw new RuntimeException(e);                   }               }           }       });       // 创建线程       myThread2.start();       while (true) {           System.out.println("主线程方法正在调用....");           try {               Thread.sleep(1000);           } catch (InterruptedException e) {               throw new RuntimeException(e);           }       }   }}AI生成项目java运行对于 匿名内部类 而已 , 使用的方式和 上面一种方式相同 , 而在这里唯一的区别就在于 , 上面 new 出了 new Runnable 作为参数 进行 传入到 Thread 对象中。而本方式中 , 则是在 Thread 后面 直接 重写 run 方法 。<4>. 使用 lambda 表达式 简化匿名内部类/** * 方法五:  使用 lambda 表达式简化 匿名内部类 * */ class  MyThread4 {    public static void main(String[] args) throws InterruptedException {        Thread myThread4 = new Thread(()->{            while (true) {                System.out.println("lambda 匿名类 MyThread的run线程 正在运行...");                try {                    MyThread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }            }        });        // 创建线程        myThread4.start();        // 输出主线程        while (true) {            System.out.println("main 线程正在运行...");            MyThread.sleep(1000);        }    }}AI生成项目java运行对于 lambda 表达式 来说,最核心的部分就是 :把 new Runnable() {} 就是简化成 ()-> {} 来使用, 其实本质上还是通过 匿名内部类 实现 Runnable 接口 的一种 简化版本 。鱼式疯言对于 lambda 表达的使用, 一定要注意一点的是: 必须是 函数式接口 , 也就是只有 一个抽象方法的接口才能使用 lambda 。对于 start 创建线程 来说, 当调用 start 方法后, 系统内核会生成 PCB 并且添加链表, 创建新的线程。 所以当 多次调用 start 方法 时, 就会抛出异常, 因为对于一个Thread 对象来说, 只能start 一次, 也就是说 只能创建一个线程 , 这个原因也是为了 JVM 方便管理 , 否则 一个Thread 对象对应多个线程 就会管理起来很复杂。例如上述过程, 就会 抛出异常 : IllegalThreadStateException上述总共 五种创建线程 的方式, 都是蛮重要的 , 小伙伴们务必多操练多熟悉里面的 代码流程和逻辑 哦~三. 线程终止1. 线程终止的初识线程终止的方式有很多种终止的方式, 有粗暴的, 也有温柔的 。像粗暴的 :让 执行到一半的线程 直接终止像温柔的: 让 线程执行完整个任务 才终止对于Java 来说, 我们是下面这种方式, 让 线程执行完所有的任务 才终止。下面就让我们来瞧瞧呗 2. Java线程终止的简易版public class MyThread1 {   public static boolean state = true;    public static void main(String[] args) throws InterruptedException {        Thread t  = new Thread(() -> {            while(state) {                System.out.println("hello Thread1");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }            }        });//        创建线程            t.start();        Thread.sleep(3000);        state = false;    }}AI生成项目java运行对于上述的流程主要是:首先定义一个布尔变量, 用来确定 线程的状态 为 true 线程正在运行, 为 false 线程结束 。其次在主线程中修改 线程状态, 让线程结束。 注意这种结束的方式是让线程中的 任务都执行结束 了 , 当需要再次执行任务的时候才 置为 false 的 , 是一种 比较温柔 的做法。鱼式疯言补充细节 : 入上图, 如果把 布尔类型放在 main 方法的内部呢?其实就会有问题, 这归咎于我们的Java语法中有个小知识点: 变量捕获其实这种变量捕获是对于 lambda 表达式 来说的, 如果要在 lambda 表达式 中使用 同一作用域下的变量 , 这个变量是 不可以被修改的 。也就是说在上面 的 state = false 出现了 修改 , 所以 lambda 中就会 编译失败 。那么出现这种问题的比较好的解决方案就是把 这个变量定义为 成员变量 , 这样 两种的作用域就不相同 了, 也从另外一个角度来看, 内部的方法调用外部的成员 是没有问题的,天经地义的3. Java 自身的线程终止方法class Thread2 {    public static void main(String[] args) {        Thread t = new Thread(() -> {            // 先获取当前线程            Thread currentThread = Thread.currentThread();//            判断线程状态            while(!currentThread.isInterrupted()) {                System.out.println("hello thread1");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    break;                }            }        });//        创建线程        t.start();        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            throw new RuntimeException(e);        }//        修改线程状态        t.interrupt();    }}AI生成项目java运行上述代码的流程主要分为 以下几步:实例化Thread对象, 在 run方法中 使用 currentThread 方法 来 获取当前线程获取当前线程后 , 使用 isInterrupted 判断当前线程的状态,自身返回 false 代表 线程正在进行, true 代表 线程结束 。创建线程,并 休眠指定时间 , 使用 interrupted 来修改 isInterrupted 为 true, 让线程终止。在 try catch{ } 在 catch 中 添加 break , 直接跳出循环 。4. 代码分析对于上述代码小编还有很多话想和小伙伴们唠唠, 因为我个人觉得还是比较重要的, 小伙伴们可不要嫌我烦哦~ 如果 catch 中不添加 break ,而是按照平常的写法: throw new RuntimeException(e); 直接抛出新的异常呢?情况就会变成这样, 原因很简单, 对于线程执行的时间来说, 大部分时间是处于 sleep 的休眠状态 , 一旦有 外面的线程来修改线程的状态 , sleep 就会 被打破休眠的状态 , 这时就会 抛出异常, 这时被 try catch { } 捕获到 , 就会执行 throw new RuntimeException(e); 抛出新的异常, 由于这个 异常没有及时处理 ,编译器就会自动交给 JVM 来处理。 所以我们不需要在 catch { } 写 throw new RuntimeException(e);的方法。居然写throw new RuntimeException(e); 会抛出新的异常, 那么 catch 中什么都不写, break 也不写。 会发生什么呢?如上图, 就会出现 即使我们使用 interrupted 来修改, 但是线程还是会继续的情况。其实大家有所不知的是: 对于 休眠方法 sleep 是比较特殊的, 一旦被唤醒, 就会清除 isInterrupted 修改后的 true 状态, 重新还原到 false 状态 , 这时线程就会 继续执行 了。所以这里如果我们要终止线程的话, 就需要 添加 break 来跳出 。相比小伙伴们还是一头雾水吧 , 就算理解了也不知道它为啥要这样做吧 ? ? ?下面削小编来举个栗子吧假如有一天小编和女神去海边浪此时小编这时坐在沙滩上打游戏突然女神过来和说: 我口渴了, 你去买杯奶茶呗~这时就凸显我以后的家庭地位了, 我就有三种选择:我听到之后没有理会, 继续打我的游戏, 从中就凸显我的 “家庭帝位” ;我立马停下手中的游戏, 马上给女神买奶茶去 ,从中就凸显我的 “家庭弟位” ;我和她说: 等我这把游戏打完, 就给你去买, 从中就凸显我的 “家庭中位” 。而上述的栗子从中也反映了: 终止线程过程中, 虽然 sleep 能够清除 isInterrupt 修改后的状态, 但是也为我们在 catch 中提供了 多方面的选择, 如果我们需要 跳出就break , 如果需要 继续执行就什么都不写 , 如果需要 执行别的业务逻辑就添加进入即可 。总结Thread 类: 对于 Thread 类 而已是 Java封装 的一种 给 系统创建线程 的一个类的概念以及使用。创建线程的方式: 掌握创建线程的主要流程和逻辑代码 , 以及熟悉这 五种线程创建的方式。线程终止: 对于线程终止的两种方式: 强行终止的粗暴方式, 等待任务结束的温柔方式 。 Java的终止线程的方式是 比较温柔并且操作空间是很大 的。————————————————原文链接:https://blog.csdn.net/mgzdwm/article/details/142483482
  • [技术干货] 【Java/数据结构】栈(Stack)(图文版)
    本博客将带大家一起学习基本数据结构之一——栈(Stack),虽然Java当中的Stack集合已经被Deque(双端队列)替代了,但是他的基本思想和实现还是有必要学习的。一.初识栈1.基本概念        堆栈又名栈(stack),它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。        向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;        从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。简单来讲,栈就是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。 如下是它在Java集合框架中的位置: ps:由于Vector设计过时,所以继承自他的Stack也被替代了。2.特性LIFO:即Last In First Out,后进先出原则。类似于坐电梯,先走进去的人后出来;或者上子弹,最先进弹夹的子弹最后打出。3.核心操作入栈(push)、出栈(pop)、查看栈顶(peek)二.栈的模拟实现老规矩,先看源码:  我们不难发现栈的实现相当简单,底层就是一个数组,同时Stack类也相当简单,仅仅只有140余行。接下来我们不考虑泛型与io,存储的数据默认为int,来实现一个简单的栈,以理解栈的底层原理。1.经典实现最经典的就是基于数组的实现:(1)基本结构public class MyStack {    private int[] elements; // 存储元素的数组    private int top;        // 栈顶指针(初始为-1)    private static final int DEFAULT_CAPACITY = 10;     // 构造方法    public MyStack() {        this(DEFAULT_CAPACITY);    }     public MyStack(int initialCapacity) {        if (initialCapacity <= 0) {            throw new IllegalArgumentException("容量必须为正数");        }        this.elements = new int[initialCapacity];        top = -1;    }        ......AI生成项目java运行说明:由于是基于数组实现的,所以不得不考虑动态扩容机制。我们提供2种构造方法,一种指定初始容量,另一种不指定,使用默认容量,即DEFAULT_CAPACITY这一静态变量。我们提供一个指针来指示栈顶,即top。(2)动态扩容// 动态扩容private void resize(int newCapacity) {    int[] newArray = new int[newCapacity];    System.arraycopy(elements, 0, newArray, 0, top + 1);    elements = newArray;}AI生成项目java运行说明:System.arraycopy(elements, 0, newArray, 0, top + 1);AI生成项目java运行复制数组参数(原数组,复制起始位置,复制目的地,目的地起始位置,复制长度)(3)入栈(push)// 入栈(带动态扩容)public void push(int value) {    // 检查是否需要扩容    if (top == elements.length - 1) {        resize(2 * elements.length);    }    elements[++top] = value;}AI生成项目java运行(4)出栈(pop)// 出栈public int pop() {    if (isEmpty()) {        throw new IllegalStateException("栈为空");    }    return elements[top--];}AI生成项目java运行(5)查看栈顶(peek)// 查看栈顶元素public int peek() {    if (isEmpty()) {        throw new IllegalStateException("栈为空");    }    return elements[top];}AI生成项目java运行(6)其他// 判断是否为空public boolean isEmpty() {    return top == -1;} // 获取元素数量public int size() {    return top + 1;}AI生成项目java运行(7)完整实现与测试public class MyStack {    private int[] elements; // 存储元素的数组    private int top;        // 栈顶指针(初始为-1)    private static final int DEFAULT_CAPACITY = 10;     // 构造方法    public MyStack() {        this(DEFAULT_CAPACITY);    }     public MyStack(int initialCapacity) {        if (initialCapacity <= 0) {            throw new IllegalArgumentException("容量必须为正数");        }        elements = new int[initialCapacity];        top = -1;    }     // 入栈(带动态扩容)    public void push(int value) {        // 检查是否需要扩容        if (top == elements.length - 1) {            resize(2 * elements.length);        }        elements[++top] = value;    }     // 出栈    public int pop() {        if (isEmpty()) {            throw new IllegalStateException("栈为空");        }        return elements[top--];    }     // 查看栈顶元素    public int peek() {        if (isEmpty()) {            throw new IllegalStateException("栈为空");        }        return elements[top];    }     // 判断是否为空    public boolean isEmpty() {        return top == -1;    }     // 获取元素数量    public int size() {        return top + 1;    }     // 动态扩容    private void resize(int newCapacity) {        int[] newArray = new int[newCapacity];        System.arraycopy(elements, 0, newArray, 0, top + 1);        elements = newArray;    }     // 测试代码    public static void main(String[] args) {        MyStack stack = new MyStack(3);         // 测试入栈和扩容        stack.push(10);        stack.push(20);        stack.push(30);        stack.push(40);  // 触发扩容到6         System.out.println("栈顶元素: " + stack.peek()); // 输出40        System.out.println("元素数量: " + stack.size()); // 输出4         // 测试出栈        System.out.println("出栈: " + stack.pop()); // 40        System.out.println("出栈: " + stack.pop()); // 30        System.out.println("剩余元素数量: " + stack.size()); // 2    }}AI生成项目java运行2.链表实现除了使用数组存储数据,使用链表也是可以的,并且使用链表不用考虑动态扩容。(1)基本结构public class MyLinkedStack {    private static class Node {        int data;        Node next;         Node(int data) {            this.data = data;        }    }     private Node top;   // 栈顶节点    private int size;   // 元素数量     ......AI生成项目java运行(2)入栈(push)public void push(int value) {    Node newNode = new Node(value);    newNode.next = top; // 新节点指向原栈顶    top = newNode;     // 更新栈顶    size++;}AI生成项目java运行(3)出栈(pop)public int pop() {    if (isEmpty()) {        throw new IllegalStateException("栈为空");    }    int value = top.data;    top = top.next; // 移动栈顶指针    size--;    return value;}AI生成项目java运行特别注意栈为空时会报错,所以要检查栈是否为空。(4)查看栈顶(peek)public int peek() {    if (isEmpty()) {        throw new IllegalStateException("栈为空");    }    return top.data;}AI生成项目java运行(5)其他public boolean isEmpty() {    return top == null;} public int size() {    return size;}AI生成项目java运行(6)完整实现与测试public class MyLinkedStack {    private static class Node {        int data;        Node next;         Node(int data) {            this.data = data;        }    }     private Node top;   // 栈顶节点    private int size;   // 元素数量     public void push(int value) {        Node newNode = new Node(value);        newNode.next = top; // 新节点指向原栈顶        top = newNode;     // 更新栈顶        size++;    }     public int pop() {        if (isEmpty()) {            throw new IllegalStateException("栈为空");        }        int value = top.data;        top = top.next; // 移动栈顶指针        size--;        return value;    }     public int peek() {        if (isEmpty()) {            throw new IllegalStateException("栈为空");        }        return top.data;    }     public boolean isEmpty() {        return top == null;    }     public int size() {        return size;    }     // 测试代码    public static void main(String[] args) {        MyLinkedStack stack = new MyLinkedStack();        stack.push(100);        stack.push(200);        System.out.println(stack.pop()); // 200        System.out.println(stack.peek()); // 100    }}AI生成项目java运行三.栈的使用请见以下代码:import java.util.Stack; public class StackDemo {    public static void main(String[] args) {        // 1. 创建栈对象        Stack<Integer> stack = new Stack<>();         // 2. 压栈操作(push)        System.out.println("----- 压栈操作 -----");        stack.push(10);        stack.push(20);        stack.push(30);        System.out.println("当前栈内容: " + stack);  // 输出: [10, 20, 30]         // 3. 查看栈顶(peek)        System.out.println("\n----- 查看栈顶 -----");        System.out.println("栈顶元素: " + stack.peek()); // 输出: 30        System.out.println("查看后栈内容: " + stack);    // 保持原样         // 4. 弹栈操作(pop)        System.out.println("\n----- 弹栈操作 -----");        System.out.println("弹出元素: " + stack.pop());  // 输出: 30        System.out.println("弹出后栈内容: " + stack);    // 输出: [10, 20]         // 5. 检查空栈(empty)        System.out.println("\n----- 检查空栈 -----");        System.out.println("栈是否为空? " + stack.empty()); // 输出: false                // 6. 搜索元素(search)        System.out.println("\n----- 搜索元素 -----");        int target = 20;        int position = stack.search(target);        System.out.println("元素 " + target + " 的位置: " + position);  // 输出: 1(栈顶为1)         // 7. 清空栈        System.out.println("\n----- 清空栈 -----");        while (!stack.empty()) {            System.out.println("弹出: " + stack.pop());        }        System.out.println("清空后栈是否为空? " + stack.empty()); // 输出: true    }}AI生成项目java运行更多信息请见官方文档说明:Stack (Java Platform SE 8 )四.栈的典型应用1.括号匹配算法该算法能自动检验输入的字符串中括号是否正确匹配:import java.util.Stack; public class BracketMatcher {    public static boolean isValid(String s) {        Stack<Character> stack = new Stack<>();         for (char c : s.toCharArray()) {            // 遇到左括号时,将对应的右括号压入栈            switch (c) {                case '(':                    stack.push(')');                    break;                case '[':                    stack.push(']');                    break;                case '{':                    stack.push('}');                    break;                default:                    // 遇到右括号时,检查栈顶是否匹配                    if (stack.isEmpty() || stack.pop() != c) {                        return false;                    }            }        }        // 最终栈必须为空才表示完全匹配        return stack.isEmpty();    }}AI生成项目java运行原理请见LeetCode:20. 有效的括号 - 力扣(LeetCode)2.逆波兰表达式(计算机的算数运算)import java.util.Stack; public class ReversePolishNotation {    public static int evalRPN(String[] tokens) {        Stack<Integer> stack = new Stack<>();                for (String token : tokens) {            // 遇到运算符时进行计算            if (isOperator(token)) {                int b = stack.pop();                int a = stack.pop();                stack.push(calculate(a, b, token));            }             // 遇到数字时压栈            else {                stack.push(Integer.parseInt(token));            }        }        return stack.pop();    }     // 判断是否是运算符    private static boolean isOperator(String s) {        return s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/");    }     // 执行运算(注意操作数顺序)    private static int calculate(int a, int b, String op) {        switch (op) {            case "+": return a + b;            case "-": return a - b;            case "*": return a * b;            case "/": return a / b;  // 题目通常要求整数除法向零取整            default: throw new IllegalArgumentException("非法运算符");        }    }     public static void main(String[] args) {        // 测试案例        String[][] testCases = {            {"2","1","+","3","*"},      // (2+1)*3=9            {"4","13","5","/","+"},      // 4+(13/5)=6            {"10","6","9","3","+","-11","*","/","*","17","+","5","+"} // 10*(6/((9+3)*-11))+17+5        };         for (String[] testCase : testCases) {            System.out.println("表达式: " + String.join(" ", testCase));            System.out.println("结果: " + evalRPN(testCase) + "\n");        }    }}AI生成项目java运行详情请见:150. 逆波兰表达式求值 - 力扣(LeetCode)结语关于用Deque替代Stack的事————————————————原文链接:https://blog.csdn.net/2401_88030885/article/details/146369622
  • [技术干货] 从零开始学java--二叉树和哈希表
     树树形结构:树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:有一个特殊的结点,称为根结点,根结点没有前驱结点。除根结点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、......、Tm,其中每一个集合Ti (1 <= i <= m)又是一棵与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。树是递归定义的。注意:树形结构中,子树之间不能有交集,否则就不是树形结构。树的概念:结点的度:一个结点含有子树的个数称为该结点的度; 如上图:A的度为6树的度:一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6叶子结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等节点为叶结点双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点根结点:一棵树中,没有双亲结点的结点;如上图:A结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推树的高度或深度:树中结点的最大层次; 如上图:树的高度为4树的以下概念只需了解,在看书时只要知道是什么意思即可:非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等节点为分支结点兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙森林:由m(m>=0)棵互不相交的树组成的集合称为森林 二叉树概念:一棵二叉树是结点的一个有限集合,该集合:1. 或者为空2. 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。二叉树不存在度大于2的结点。 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。对于任意的二叉树都是由以下几种情况复合而成的: 两种特殊的二叉树:1. 满二叉树: 一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵二叉树的层数为K,且结点总数是 2的k次方-1 ,则它就是满二叉树。2. 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。  二叉树的性质:1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 (i>0)个结点2. 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 (k>=0)3. 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+14. 具有n个结点的完全二叉树的深度k为 上取整5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i 的结点有:若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点若2i+1,左孩子序号:2i+1,否则无左孩子若2i+2,右孩子序号:2i+2,否则无右孩子创建一个简单的二叉树:public class Main {    public static void main(String[] args) {        TreeNode<Character>a=new TreeNode<>('A');        TreeNode<Character>b=new TreeNode<>('B');        TreeNode<Character>c=new TreeNode<>('C');        TreeNode<Character>d=new TreeNode<>('D');        TreeNode<Character>e=new TreeNode<>('E');         a.left=b;        a.right=c;        b.left=d;        b.right=e;         System.out.println(a.left.left.element);    }     public static class TreeNode<E>{        public E element;        public TreeNode<E> left,right;         public TreeNode(E element){            this.element=element;        }    }} //输出DAI生成项目java运行二叉树的遍历所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结 点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如:打印节点内容、节点内容加 1)。 遍历是二叉树上最重要的操作之一,是二叉树上进行其它运算之基础。 前序遍历:打印根节点前序遍历左子树前序遍历右子树public class Main {    public static void main(String[] args) {        TreeNode<Character>a=new TreeNode<>('A');        TreeNode<Character>b=new TreeNode<>('B');        TreeNode<Character>c=new TreeNode<>('C');        TreeNode<Character>d=new TreeNode<>('D');        TreeNode<Character>e=new TreeNode<>('E');        TreeNode<Character>f=new TreeNode<>('F');        TreeNode<Character>g=new TreeNode<>('G');        TreeNode<Character>h=new TreeNode<>('H');         a.left=b;        a.right=c;        b.left=d;        b.right=e;        e.left=h;        c.left=f;        c.right=g;         preOrder(a);    }     public static void preOrder(TreeNode<Character> root){        if(root==null)return;        System.out.print(root.element+" ");        preOrder(root.left);        preOrder(root.right);    }     public static class TreeNode<E>{        public E element;        public TreeNode<E> left,right;         public TreeNode(E element){            this.element=element;        }    }}//输出A B D E H C F GAI生成项目java运行 ABDEHCFG中序遍历:中序遍历左子树打印结点中序遍历右子树public static void inOrder(TreeNode<Character>root){        if(root==null)return;        inOrder(root.left);        System.out.print(root.element+" ");        inOrder(root.right);    }//输出D B H E A F C G AI生成项目java运行DBEHAFCG后序遍历:后序遍历左子树后序遍历右子树打印结点    public static void postOrder(TreeNode<Character>root){        if(root==null)return;        postOrder(root.left);        postOrder(root.right);        System.out.print(root.element+" ");    }//输出D H E B F G C A AI生成项目java运行DHEBFGCA层序遍历:利用队列来实现层序遍历,首先将根节点存入队列中,接着循环执行以下步骤:进行出队操作,得到一个结点,并打印结点的值将此结点的左右孩子结点依次入队    public static void levelOrder(TreeNode<Character>root){        LinkedQueue<TreeNode<Character>> queue=new LinkedQueue<>(); //创建一个队列        queue.offer(root); //将根结点丢进队列        while (!queue.isEmpty()){ //如果队列不为空,就一直不断的取出来            TreeNode<Character>node=queue.poll();  //取一个出来            System.out.print(node.element+" ");  //打印            if (node.left!=null)queue.offer(node.left);  //如果左右孩子不为空,直接将左右孩子丢进队列            if (node.right!=null)queue.offer(node.right);        }    }//输出A B C D E F G H AI生成项目java运行 二叉查找树和平衡二叉树二叉查找树:二叉查找树也叫二叉搜索树或二叉排序树左子树中所有结点的值,均小于其根结点的值右子树中所有结点的值,均大于其根结点的值二叉搜索树的子树也是二叉搜索树平衡二叉树:在插入结点时要尽可能避免一边倒的情况,引入平衡二叉树的概念,在插入时如果不维护二叉树的平衡,某一边只会无限制的延伸下去,出现极度不平衡的情况。平衡二叉树一定是一颗二叉查找树任意结点的左右子树也是一颗平衡二叉树从根结点开始,左右子树高度差都不能超过1,否则视为不平衡二叉树上结点的左子树高度 减去 右子树高度,得到的结果称为该节点的平衡因子失衡情况的调整:1、LL型调整(右旋)2、RR型调整(左旋)3、RL型调整(先右旋再左旋)4、LR型调整(先左旋再右旋)红黑树红黑树也是二叉查找树的一种,结点有红有黑。规则1:每个结点可以是黑色或红色规则2:根结点一定是黑色规则3:红色结点的父结点和子结点不能为红色(不能有两个连续的红色)规则4:所有的空结点都是黑色(空结点视为null,红黑树中是将空结点视为叶子结点)规则5:每个结点到空结点路径上出现的黑色结点的个数都相等哈希表散列表散列(Hashing)通过散列函数(哈希函数)将需要参与检索的数据与散列值(哈希值)关联起来,生成一种便于搜索的数据结构,我们称其为散列表(哈希表)。散列函数也加哈希函数,哈希函数可以对一个目标计算出其对应的哈希值,并且,只要是同一个目标,无论计算多少次,得到的哈希值都是一样的结果,不同的目标计算出的结果几乎都不同,哈希函数在现实生活中应用十分广泛,比如很多下载网站都提供下载文件的MD5码校验,可以用来判别文件是否完整,哈希函数多种多样,目前应用最为广泛的是SHA-1和MD5。我们可以利用哈希值的特性,设计一张全新的表结构,这种表结构是专门为哈希设立的,我们称其为哈希表。我们可以将这些元素保存到哈希表中,而保存的位置则与其对应的哈希值有关,哈希值是通过哈希函数计算得到的,我们只需要将对应元素的关键字(一般是整数)提供给哈希函数就可以进行计算了,一般比较简单的哈希函数就是取模操作,哈希表长度是多少(长度最好是一个素数),模就是多少。保存的数据是无序的,哈希表在查找时只需要进行一次哈希函数计算就能直接找到对应元素的存储位置,效率极高。public class HashTable<E> {    private final int TABLE_SIZE=10;    private final Object[]TABLE=new Object[TABLE_SIZE];     //插入    public void insert(E obj){        int index=hash(obj);        TABLE[index]=obj;    }     //判断是否包含    public boolean contains(E obj){        int index=hash(obj);        return TABLE[index]==obj;    }     private int hash(E obj){ //哈希函数,计算出存放的位置        int hashCode=obj.hashCode();        //每一个对象都有一个独一无二的哈希值,可以通过hashCode方法得到(极小概率出现相同情况)        return hashCode%TABLE_SIZE;    }} import com.test.collection.HashTable;     public static void main(String[] args) {        HashTable<String>table=new HashTable<>();        String str="AAA";        System.out.println(table.contains(str));        table.insert(str);        System.out.println(table.contains(str));    } //输出false//trueAI生成项目java运行通过哈希函数计算得到一个目标的哈希值,但是在某些情况下哈希值可能会出现相同的情况,称为哈希碰撞(哈希冲突)常见的哈希冲突解决方案是链地址法,当出现哈希冲突时,我们依然将其保存在对应的位置上,我们可以将其连接为一个链表的形式:package com.test.collection; public class HashTable<E> {    private final int TABLE_SIZE=10;    private final Node[]TABLE=new Node[TABLE_SIZE];     //放入头结点    public HashTable(){        for (int i = 0; i < TABLE_SIZE; i++)            TABLE[i]=new Node<>(null);    }     //插入    public void insert(E obj){        int index=hash(obj);        Node<E>head=TABLE[index];        Node<E>node=new Node<>(obj);        node.next=head.next;        head.next=node;    }     //判断是否包含    public boolean contains(E element){        int index=hash(element);        Node<E>node=TABLE[index].next;        while (node!=null){            if(node.element==element)                return true;            node=node.next;        }        return false;    }     private int hash(E obj){ //哈希函数,计算出存放的位置        int hashCode=obj.hashCode();        //每一个对象都有一个独一无二的哈希值,可以通过hashCode方法得到(极小概率出现相同情况)        return hashCode%TABLE_SIZE;    }     public String toString(){        StringBuilder builder=new StringBuilder();        for (int i = 0; i < TABLE_SIZE; i++) {            Node<E>head=TABLE[i].next;            while (head!=null){                builder.append(head.element+"->");                head=head.next;            }            builder.append("\n");        }        return builder.toString();    }      private static class Node<E>{        private final E element;        private Node<E> next;         private Node(E element){            this.element=element;        }    }}AI生成项目java运行    public static void main(String[] args) {         HashTable<Integer>table1=new HashTable<>();        for (int i = 0; i < 100; i++)            table1.insert(i);         System.out.println(table1);    } /*输出90->80->70->60->50->40->30->20->10->0->91->81->71->61->51->41->31->21->11->1->92->82->72->62->52->42->32->22->12->2->93->83->73->63->53->43->33->23->13->3->94->84->74->64->54->44->34->24->14->4->95->85->75->65->55->45->35->25->15->5->96->86->76->66->56->46->36->26->16->6->97->87->77->67->57->47->37->27->17->7->98->88->78->68->58->48->38->28->18->8->99->89->79->69->59->49->39->29->19->9->————————————————原文链接:https://blog.csdn.net/PluMage11/article/details/147387863 
  • [技术干货] 【Java 开发日记】一个不注意就死锁了,该怎么办呢
    向现实世界要答案现实世界里,账户转账操作是支持并发的,而且绝对是真正的并行,银行所有的窗口都可以做转账操作。只要我们能仿照现实世界做转账操作,串行的问题就解决了。我们试想在古代,没有信息化,账户的存在形式真的就是一个账本,而且每个账户都有一个账本,这些账本都统一存放在文件架上。银行柜员在给我们做转账时,要去文件架上把转出账本和转入账本都拿到手,然后做转账。这个柜员在拿账本的时候可能遇到以下三种情况:文件架上恰好有转出账本和转入账本,那就同时拿走;如果文件架上只有转出账本和转入账本之一,那这个柜员就先把文件架上有的账本拿到手,同时等着其他柜员把另外一个账本送回来;转出账本和转入账本都没有,那这个柜员就等着两个账本都被送回来。上面这个过程在编程的世界里怎么实现呢?其实用两把锁就实现了,转出账本一把,转入账本另一把。在transfer()方法内部,我们首先尝试锁定转出账户this(先把转出账本拿到手),然后尝试锁定转入账户target(再把转入账本拿到手),只有当两者都成功时,才执行转账操作。这个逻辑可以图形化为下图这个样子。 两个转账操作并行示意图而至于详细的代码实现,如下所示。经过这样的优化后,账户A 转账户B和账户C 转账户D这两个转账操作就可以并行了。class Account {  private int balance;  // 转账  void transfer(Account target, int amt){    // 锁定转出账户    synchronized(this) {      // 锁定转入账户      synchronized(target) {        if (this.balance > amt) {          this.balance -= amt;          target.balance += amt;        }      }    }  }} 运行本项目没有免费的午餐上面的实现看上去很完美,并且也算是将锁用得出神入化了。相对于用Account.class作为互斥锁,锁定的范围太大,而我们锁定两个账户范围就小多了,这样的锁,上一章我们介绍过,叫 细粒度锁。 使用细粒度锁可以提高并行度,是性能优化的一个重要手段。这个时候可能你已经开始警觉了,使用细粒度锁这么简单,有这样的好事,是不是也要付出点什么代价啊?编写并发程序就需要这样时时刻刻保持谨慎。的确,使用细粒度锁是有代价的,这个代价就是可能会导致死锁。在详细介绍死锁之前,我们先看看现实世界里的一种特殊场景。如果有客户找柜员张三做个转账业务:账户A 转账户B 100元,此时另一个客户找柜员李四也做个转账业务:账户B 转账户A 100 元,于是张三和李四同时都去文件架上拿账本,这时候有可能凑巧张三拿到了账本A,李四拿到了账本B。张三拿到账本A后就等着账本B(账本B已经被李四拿走),而李四拿到账本B后就等着账本A(账本A已经被张三拿走),他们要等多久呢?他们会永远等待下去…因为张三不会把账本A送回去,李四也不会把账本B送回去。我们姑且称为死等吧。​转账业务中的“死等”现实世界里的死等,就是编程领域的死锁了。 死锁 的一个比较专业的定义是: 一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。上面转账的代码是怎么发生死锁的呢?我们假设线程T1执行账户A转账户B的操作,账户A.transfer(账户B);同时线程T2执行账户B转账户A的操作,账户B.transfer(账户A)。当T1和T2同时执行完①处的代码时,T1获得了账户A的锁(对于T1,this是账户A),而T2获得了账户B的锁(对于T2,this是账户B)。之后T1和T2在执行②处的代码时,T1试图获取账户B的锁时,发现账户B已经被锁定(被T2锁定),所以T1开始等待;T2则试图获取账户A的锁时,发现账户A已经被锁定(被T1锁定),所以T2也开始等待。于是T1和T2会无期限地等待下去,也就是我们所说的死锁了。class Account {  private int balance;  // 转账  void transfer(Account target, int amt){    // 锁定转出账户    synchronized(this){     ①      // 锁定转入账户      synchronized(target){ ②        if (this.balance > amt) {          this.balance -= amt;          target.balance += amt;        }      }    }  }} 运行本项目关于这种现象,我们还可以借助资源分配图来可视化锁的占用情况(资源分配图是个有向图,它可以描述资源和线程的状态)。其中,资源用方形节点表示,线程用圆形节点表示;资源中的点指向线程的边表示线程已经获得该资源,线程指向资源的边则表示线程请求资源,但尚未得到。转账发生死锁时的资源分配图就如下图所示,一个“各据山头死等”的尴尬局面。 转账发生死锁时的资源分配图如何预防死锁并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题最好的办法还是规避死锁。那如何避免死锁呢?要避免死锁就需要分析死锁发生的条件,有个叫Coffman的牛人早就总结过了,只有以下这四个条件都发生时才会出现死锁:互斥,共享资源X和Y只能被一个线程占用;占有且等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;不可抢占,其他线程不能强行抢占线程T1占有的资源;循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。反过来分析, 也就是说只要我们破坏其中一个,就可以成功避免死锁的发生。其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。我们已经从理论上解决了如何预防死锁,那具体如何体现在代码上呢?下面我们就来尝试用代码实践一下这些理论。1. 破坏占用且等待条件从理论上讲,要破坏这个条件,可以一次性申请所有资源。在现实世界里,就拿前面我们提到的转账操作来讲,它需要的资源有两个,一个是转出账户,另一个是转入账户,当这两个账户同时被申请时,我们该怎么解决这个问题呢?可以增加一个账本管理员,然后只允许账本管理员从文件架上拿账本,也就是说柜员不能直接在文件架上拿账本,必须通过账本管理员才能拿到想要的账本。例如,张三同时申请账本A和B,账本管理员如果发现文件架上只有账本A,这个时候账本管理员是不会把账本A拿下来给张三的,只有账本A和B都在的时候才会给张三。这样就保证了“一次性申请所有资源”。 通过账本管理员拿账本对应到编程领域,“同时申请”这个操作是一个临界区,我们也需要一个角色(Java里面的类)来管理这个临界区,我们就把这个角色定为Allocator。它有两个重要功能,分别是:同时申请资源apply()和同时释放资源free()。账户Account 类里面持有一个Allocator的单例(必须是单例,只能由一个人来分配资源)。当账户Account在执行转账操作的时候,首先向Allocator同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,我们需通知Allocator同时释放转出账户和转入账户这两个资源。具体的代码实现如下。class Allocator {  private List<Object> als =    new ArrayList<>();  // 一次性申请所有资源  synchronized boolean apply(    Object from, Object to){    if(als.contains(from) ||         als.contains(to)){      return false;    } else {      als.add(from);      als.add(to);    }    return true;  }  // 归还资源  synchronized void free(    Object from, Object to){    als.remove(from);    als.remove(to);  }} class Account {  // actr应该为单例  private Allocator actr;  private int balance;  // 转账  void transfer(Account target, int amt){    // 一次性申请转出账户和转入账户,直到成功    while(!actr.apply(this, target))      ;    try{      // 锁定转出账户      synchronized(this){        // 锁定转入账户        synchronized(target){          if (this.balance > amt){            this.balance -= amt;            target.balance += amt;          }        }      }    } finally {      actr.free(this, target)    }  }} 运行本项目2. 破坏不可抢占条件破坏不可抢占条件看上去很简单,核心是要能够主动释放它占有的资源,这一点synchronized是做不到的。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。你可能会质疑,“Java作为排行榜第一的语言,这都解决不了?”你的怀疑很有道理,Java在语言层次确实没有解决这个问题,不过在SDK层面还是解决了的,java.util.concurrent这个包下面提供的Lock是可以轻松解决这个问题的。关于这个话题,咱们后面会详细讲。3. 破坏循环等待条件破坏这个条件,需要对资源进行排序,然后按序申请资源。这个实现非常简单,我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。比如下面代码中,①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。class Account {  private int id;  private int balance;  // 转账  void transfer(Account target, int amt){    Account left = this            Account right = target;        if (this.id > target.id) {       left = target;                 right = this;                }                              // 锁定序号小的账户    synchronized(left){      // 锁定序号大的账户      synchronized(right){        if (this.balance > amt){          this.balance -= amt;          target.balance += amt;        }      }    }  }} 运行本项目总结当我们在编程世界里遇到问题时,应不局限于当下,可以换个思路,向现实世界要答案, 利用现实世界的模型来构思解决方案,这样往往能够让我们的方案更容易理解,也更能够看清楚问题的本质。但是现实世界的模型有些细节往往会被我们忽视。因为在现实世界里,人太智能了,以致有些细节实在是显得太不重要了。在转账的模型中,我们为什么会忽视死锁问题呢?原因主要是在现实世界,我们会交流,并且会很智能地交流。而编程世界里,两个线程是不会智能地交流的。所以在利用现实模型建模的时候,我们还要仔细对比现实世界和编程世界里的各角色之间的差异。我们今天这一篇文章主要讲了 用细粒度锁来锁定多个资源时,要注意死锁的问题。这个就需要你能把它强化为一个思维定势,遇到这种场景,马上想到可能存在死锁问题。当你知道风险之后,才有机会谈如何预防和避免,因此, 识别出风险很重要。预防死锁主要是破坏三个条件中的一个,有了这个思路后,实现就简单了。但仍需注意的是,有时候预防死锁成本也是很高的。例如上面转账那个例子,我们破坏占用且等待条件的成本就比破坏循环等待条件的成本高,破坏占用且等待条件,我们也是锁了所有的账户,而且还是用了死循环 while(!actr.apply(this, target)); 方法,不过好在apply()这个方法基本不耗时。 在转账这个例子中,破坏循环等待条件就是成本最低的一个方案。所以我们在选择具体方案的时候,还需要 评估一下操作成本,从中选择一个成本最低的方案。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/150211720
  • [分享交流] Github代码fork之后与原仓库进行同步
    前言在使用GitHub时,看到好的项目或想给某个项目做贡献,此时通常会将代码仓库fork到自己的账号下。如果在此期间,如果源仓库的代码发生了变动,就需要与源仓库代码进行同步。这里实操一下,如何实现这一操作。 配置项目的上游仓库首先需要大家将fork的仓库代码clone到本地,后面的所有操作都是基于本地代码库来进行操作的。比如,可以通过git clone先将fork的代码下载到本地:git clone git@github.com:secbr/nacos.git后续的一步步操作,都是基于本地仓库来进行操作。 进入到本地仓库目录通过cd操作,进入到clone下来的本地仓库根目录:cd /Users/apple/develop/nacos-request/nacos后续的操作无特殊说明,都是在这个本地仓库的目录下进行操作。 查看远程仓库路径执行命令 git remote -v 查看远程仓库的路径:appledeMacBook-Pro-2:nacos apple$ git remote -vorigin https://github.com/secbr/nacos.git (fetch)origin https://github.com/secbr/nacos.git (push)如果只显示2行内容,说明该项目还未设置upstream(中文叫:上游代码库),一般情况下,设置好一次upstream后就无需重复设置。通过显示远程仓库的路径和clone时的路径对照,会发现,此时远程仓库的路径还是fork项目的路径。 添加upstream路径执行命令 git remote add upstream https://xxx.git,把fork的源仓库设置为 upstream 。这里项目是从alibaba的nacos仓库fork过来的,因此对应的upstream就是alibaba的源仓库地址。执行上述命令,在此执行git remote -v 检查是否成功。appledeMacBook-Pro-2:nacos apple$ git remote add upstream https://github.com/alibaba/nacos.gitappledeMacBook-Pro-2:nacos apple$ git remote -vorigin https://github.com/secbr/nacos.git (fetch)origin https://github.com/secbr/nacos.git (push)upstream https://github.com/alibaba/nacos.git (fetch)upstream https://github.com/alibaba/nacos.git (push)通过上面的输出可以看出,多了两项upstream的地址,说明添加upstream成功。 检查本地代码状态由于实例是直接从仓库clone下来的,本地还没有修改代码。如果本地项目已经修改了一些代码,不确定是否提交了代码,就需要执行git status来检查一下。appledeMacBook-Pro-2:nacos apple$ git statusOn branch developYour branch is up to date with 'origin/develop'. nothing to commit, working tree clean上面显示,本地没有需要提交的(commit)的代码。如果本地有修改,需要先从本地仓库推送到GitHub仓库。然后,再执行一次 git status 检查。对应推送到GitHub仓库的基本操作步骤如下:git add -A 或者 git add filenamegit commit -m "your note"git push origin mastergit status完成上面的基本操作之后,确认代码都已经提交,便可以开始执行源仓库与本地仓库的merge操作了。 抓取源仓库的更新经过上面步骤的准备之后,可以进行源仓库的代码更新了。执行命令 git fetch upstream 抓取原仓库的更新:appledeMacBook-Pro-2:nacos apple$ git fetch upstreamremote: Enumerating objects: 2646, done.remote: Counting objects: 100% (2593/2593), done.remote: Compressing objects: 100% (1157/1157), done.remote: Total 2646 (delta 731), reused 2404 (delta 682), pack-reused 53Receiving objects: 100% (2646/2646), 1.67 MiB | 1.47 MiB/s, done.Resolving deltas: 100% (734/734), completed with 37 local objects.From https://github.com/alibaba/nacos* [new branch] 0.2.1 -> upstream/0.2.1* [new branch] 0.2.2 -> upstream/0.2.2* [new branch] 0.3.0 -> upstream/0.3.0//...省略一部分执行上述命令之后,上游仓库的更新(commit)会本存储为本地的分支,通常名称为:upstream/BRANCHNAME。比如上面的upstream/0.3.0。 切换分支完成了上游仓库分支的拉取之后,先来核查一下本地仓库当前处于哪个分支,也就是需要更新合并的分支。比如,这里需要将develop分支的内容更新到与上游仓库代码一致。则先切换到develop分支:appledeMacBook-Pro-2:nacos apple$ git checkout developAlready on 'develop'Your branch is up to date with 'origin/develop'.上面提示已经是develop分支了。 执行合并执行命令 git merge upstream/develop 合并远程的develop分支。比如合并的可能是master,可根据需要将develop的名称替换成对应的master。appledeMacBook-Pro-2:nacos apple$ git merge upstream/developRemoving test/src/test/java/com/alibaba/nacos/test/naming/DeregisterInstance_ITCase.java// ...省略一部分Removing naming/src/test/java/com/alibaba/nacos/naming/core/PushServiceTest.javaAuto-merging client/src/main/java/com/alibaba/nacos/client/naming/remote/http/NamingHttpClientProxy.javaCONFLICT (content): Merge conflict in client/src/main/java/com/alibaba/nacos/client/naming/remote/http/NamingHttpClientProxy.javaRemoving client/src/main/java/com/alibaba/nacos/client/naming/core/HostReactor.javaRemoving .editorconfigAutomatic merge failed; fix conflicts and then commit the result.执行完上述命令之后,会发现上游代码指定分支的修改内容已经反映到本地代码了。 上传代码到fork分支执行完上一步的合并操作之后,往后还有一些后续处理,比如代码冲突。如果本地修改了内容,上游仓库也修改了对应的代码,则可能会出现冲突。这时就需要对比代码进行修改。本人更习惯使用IDEA中可视化的插件进行代码冲突解决,也可以选择喜欢的方式进行解决。解决完冲突之后,就可以执行正常的代码add、commit和push操作了。这里的一系列操作都是针对自己fork的仓库的,对应操作实例如下:appledeMacBook-Pro-2:nacos apple$ git add .appledeMacBook-Pro-2:nacos apple$ git commit -m 'merge from nacos'[develop 8601c1791] merge from nacos appledeMacBook-Pro-2:nacos apple$ git pushEnumerating objects: 4, done.Counting objects: 100% (4/4), done.Delta compression using up to 12 threadsCompressing objects: 100% (2/2), done.Writing objects: 100% (2/2), 281 bytes | 281.00 KiB/s, done.Total 2 (delta 1), reused 0 (delta 0), pack-reused 0remote: Resolving deltas: 100% (1/1), completed with 1 local object.To https://github.com/secbr/nacos.git76a4dcbb1..8601c1791 develop -> develop上述操作,通过add、commit、push一系列操作,将源仓库中的修改内容,提交到自己fork的分支当中了。此时再查看自己fork的GitHub仓库,可以发现代码已经更新。 
  • [技术干货] Java报错:org.springframework.beans.factory.BeanCreationException的五种解决方法【转载】
    在Java开发的复杂生态中,尤其是当使用Spring框架来构建强大的应用程序时,异常情况就像隐藏在暗处的荆棘,随时可能阻碍开发进程。其中,org.springframework.beans.factory.BeanCreationException这个异常犹如一位不速之客,常常在Bean的创建阶段引发混乱。它的出现意味着在Spring容器尝试创建Bean的过程中遇到了严重问题,而这些问题可能隐藏在看似简单的配置或者复杂的代码逻辑之中。这不仅让开发者们感到困惑,也可能影响整个应用程序的正常启动和运行。那么,如何才能像经验丰富的侦探一样,迅速找出问题所在并解决这个令人头疼的报错呢?本文将为您详细剖析这个异常,提供一系列实用的解决方案。一、问题描述1.1 报错示例以下是一个简单但具有代表性的场景,可能导致org.springframework.beans.factory.BeanCreationException异常的出现:假设我们有一个简单的Java类,代表一个用户信息的实体类:1234567891011121314151617package com.example.model;public class User {    private String username;    private int age;    public String getUsername() {        return username;    }    public void setUsername(String username) {        this.username = username;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }}然后,我们创建一个Spring的配置文件(beans.xml)来配置这个User类的Bean:12345678910<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd">    <bean id="user" class="com.example.model.User">        <property name="username" value="John Doe"/>        <property name="age" value="abc"/> <!-- 这里故意设置一个错误的值类型,应为整数 -->    </bean></beans>最后,我们创建一个简单的测试类来获取这个Bean:1234567891011package com.example.test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import com.example.model.User;public class Main {    public static void main(String[] args) {        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");        User user = (User) context.getBean("user");        System.out.println("Username: " + user.getUsername() + ", Age: " + user.getAge());    }}在这个示例中,由于在配置文件中为age属性设置了一个错误类型的值(字符串abc而不是整数),当Spring尝试创建user Bean时,就可能会抛出org.springframework.beans.factory.BeanCreationException异常。1.2 报错分析在上述示例中,导致org.springframework.beans.factory.BeanCreationException异常的原因主要是由于Bean属性配置错误。1.2.1 类型不匹配问题在配置user Bean时,对于age属性,Spring期望一个整数类型的值,但配置文件中提供了一个字符串。当Spring尝试将字符串abc转换为整数来设置age属性时,转换失败,从而导致Bean创建过程中的异常。这种类型不匹配问题在Spring的属性注入机制中是一个常见的错误来源。1.2.2 Bean依赖问题(假设存在依赖)如果User类依赖于其他的Bean,而这些依赖Bean在创建过程中出现问题(例如,依赖Bean的配置错误、无法找到依赖Bean的类等),也会导致user Bean创建失败。比如,如果User类有一个Address类型的属性,而Address Bean的配置存在问题,那么在创建user Bean时,当Spring尝试注入Address Bean时就会出现异常。1.2.3 类加载问题有时候,即使Bean本身的配置看起来正确,但如果类加载出现问题,也会导致Bean无法创建。例如,如果User类所在的jar包损坏或者类路径配置错误,Spring在尝试加载User类时会失败,进而引发BeanCreationException。这可能是由于项目构建问题、IDE设置问题或者运行环境问题导致的。1.3 解决思路解决这个问题的思路主要是从Bean的配置、依赖关系以及类加载等方面进行全面排查。1.3.1 检查Bean属性配置仔细检查配置文件中对Bean属性的设置,包括属性值的类型、格式等是否正确。确保每个属性的值都能正确地转换为Bean类中相应属性的类型。1.3.2 审查Bean依赖关系对于有依赖的Bean,检查依赖Bean的配置是否正确,是否存在依赖缺失或者配置错误的情况。可以通过查看Spring的启动日志或者使用调试工具来追踪依赖Bean的创建过程。1.3.3 排查类加载问题确认Bean相关类的类路径是否正确,检查项目的构建路径、IDE设置以及运行环境的配置。确保类能够被正确加载,没有受到损坏的jar包或者错误的类路径影响。二、解决方法2.1 方法一:修正Bean属性配置错误2.1.1 类型检查与修正在上述示例中,对于user Bean的age属性,将配置文件中的错误值修改为正确的整数类型。修改后的配置如下:12345678910<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd">    <bean id="user" class="com.example.model.User">        <property name="username" value="John Doe"/>        <property name="age" value="30"/>    </bean></beans>这样,Spring在创建user Bean时就能正确地设置age属性,避免因类型不匹配导致的异常。2.1.2 语法和格式检查除了类型问题,还要检查属性配置的语法和格式。例如,如果属性值是一个复杂的表达式或者引用其他Bean,确保语法正确。如果使用SpEL(Spring Expression Language)表达式,检查表达式的语法和引用是否正确。2.2 方法二:解决Bean依赖问题2.2.1 检查依赖Bean的配置如果User类依赖于其他Bean,如Address Bean,检查Address Bean的配置文件。确保Address Bean的类名、属性配置等都正确。例如,如果Address Bean的配置如下:1234<bean id="address" class="com.example.model.Address">    <property name="street" value="123 Main St"/>    <property name="city" value="Anytown"/></bean>检查Address类是否存在,属性street和city是否与Address类中的属性匹配,以及类路径是否正确。2.2.2 处理依赖缺失或错误如果发现依赖Bean缺失(例如,忘记配置某个依赖Bean),添加正确的配置。如果依赖Bean的配置存在错误,如类名错误或者属性设置问题,修改相应的配置。另外,如果存在循环依赖问题(即两个或多个Bean相互依赖),可以采用合适的设计模式或者使用Spring的@Lazy注解来解决。例如,如果A Bean依赖于B Bean,B Bean又依赖于A Bean,可以在其中一个Bean的依赖注入处使用@Lazy注解,延迟该Bean的初始化,以避免循环依赖导致的创建失败。2.3 方法三:解决类加载问题2.3.1 检查项目构建路径在使用IDE(如Eclipse、IntelliJ IDEA等)开发时,检查项目的构建路径设置。确保所有包含Bean相关类的源文件目录、依赖的jar包等都正确添加到构建路径中。在Eclipse中,可以通过项目属性中的“Java Build Path”选项来检查和修改构建路径。在IntelliJ IDEA中,可以在项目结构设置中查看和调整相关内容。2.3.2 检查运行环境和类路径当应用程序在服务器或者其他运行环境中部署时,检查运行环境的类路径设置。确保所有必要的jar包都在类路径中,并且没有冲突或损坏的jar包。如果使用了Maven或Gradle等构建工具,检查生成的可执行jar包或war包的结构,确保类和依赖的jar包都正确打包。如果发现类加载问题是由于损坏的jar包导致的,可以尝试重新下载或更新相关的jar包。2.3.3 查看类加载日志大多数Java应用服务器和运行环境都提供了类加载的日志功能。可以查看这些日志来确定是否存在类加载失败的情况。例如,在Tomcat服务器中,可以查看catalina.out或localhost.log等日志文件,查找有关类加载问题的信息。根据日志中的错误信息,如“ClassNotFoundException”或“java.lang.NoClassDefFoundError”,进一步排查问题所在。2.4 方法四:使用Spring的调试和日志功能2.4.1 启用详细的Spring日志在项目的日志配置文件(如log4j.properties或logback.xml等)中,将Spring相关的日志级别设置为DEBUG或TRACE。例如,在log4j.properties中,可以添加以下内容:1log4j.logger.org.springframework=DEBUG这样,在应用程序启动和运行过程中,Spring会输出更详细的日志信息,包括Bean的创建过程、属性注入情况、依赖关系处理等。通过查看这些详细日志,可以更清晰地发现问题所在。例如,如果Bean创建失败,日志中可能会显示出具体是哪个属性配置错误、哪个依赖Bean无法创建或者是类加载过程中的哪个步骤出现问题。2.4.2 使用Spring的调试工具(在IDE中)许多现代的IDE(如IntelliJ IDEA)提供了对Spring的调试支持。在调试模式下启动应用程序,可以在IDE中设置断点,观察Spring在创建Bean过程中的内部操作。可以查看变量的值、方法的调用顺序等,帮助确定问题的具体位置。例如,可以在Spring创建user Bean的过程中,在属性注入的代码处设置断点,查看注入的值是否正确,以及是否有异常抛出。三、其他解决方法检查与其他框架或组件的集成:如果应用程序使用了多个框架或组件,并且它们与Spring有交互,检查这些交互是否导致了BeanCreationException。例如,如果使用Spring与Hibernate集成,检查Hibernate的配置是否与Spring的配置相互影响,是否存在版本冲突或者配置不兼容的问题。确保所有相关框架和组件的配置都正确协调,不会干扰Spring的Bean创建过程。清理和重新构建项目:有时候,由于编译过程中的临时文件、缓存或者其他原因,可能会导致Bean创建出现问题。可以尝试在IDE中清理项目(通常有专门的清理选项),然后重新构建项目。这可以清除可能存在的错误编译产物,重新生成干净的类文件和配置文件。在使用Maven或Gradle等构建工具时,也可以使用相应的清理和重新构建命令(如mvn clean install或gradle clean build)。检查Bean的生命周期方法(如果存在):如果Bean实现了InitializingBean接口或者定义了init - method等生命周期方法,检查这些方法中的代码是否存在问题。例如,如果在InitializingBean的afterPropertiesSet方法中抛出异常,也会导致Bean创建失败。同样,对于destroy - method(如果有)也要进行检查,确保在Bean的整个生命周期中不会出现影响其创建的问题。可以在这些生命周期方法中添加日志输出,或者使用调试工具来检查方法的执行情况。四、总结本文围绕org.springframework.beans.factory.BeanCreationException这个在Spring应用程序开发中常见的异常展开了深入的讨论。通过详细的代码示例展示了可能导致该异常的场景,包括Bean属性配置错误、Bean依赖问题以及类加载问题等。分析了这些问题引发异常的原因,强调了在Bean创建过程中各个环节出现问题的可能性。提出了多种解决方法,如修正Bean属性配置错误、解决Bean依赖问题、解决类加载问题以及使用Spring的调试和日志功能等。此外,还介绍了检查与其他框架或组件的集成、清理和重新构建项目以及检查Bean的生命周期方法等其他相关的解决途径。下次遇到这类报错时,首先应该查看Spring的日志信息(如果已启用详细日志),以确定问题的大致方向。然后从Bean的属性配置、依赖关系、类加载以及与其他框架的集成等方面入手,逐一排查可能出现的问题。结合本文提到的各种方法,全面检查和修复问题,确保Spring能够顺利创建Bean,保障应用程序的正常启动和运行。在开发和维护过程中,保持对配置文件和代码的仔细审查,以及对运行环境和框架集成的关注,有助于预防和及时解决这类异常情况,提高应用程序的稳定性和可靠性。
  • [技术干货] java -jar example.jar 产生的日志输出到指定文件的方法【转载】
    一、方法1:使用重定向1、在命令行中,你可以使用重定向操作符 > 或 >> 来将输出重定向到文件中。例如:12$  java -jar example.jar > output.log$  java -jar example.jar >> output.log2、这会将标准输出(stdout)重定向到 output.log 文件。如果你想同时捕获标准错误(stderr),可以使用 2>&1 来合并标准错误到标准输出:12java -jar example.jar > output.log 2>&1Java -jar example.jar >> output.log 2>&1二、方法2:在代码中配置日志框架1、如果你使用的是如 Log4j、SLF4J、Logback 等日志框架,你可以在代码中配置日志的输出目的地。例如,使用 Logback 的 logback.xml 配置文件:1234567891011<configuration>    <appender name="FILE" class="ch.qos.logback.core.FileAppender">        <file>path/to/your/logfile.log</file>        <encoder>            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>        </encoder>    </appender>    <root level="debug">        <appender-ref ref="FILE" />    </root></configuration>2、确保将 标签中的路径改为你的目标文件路径。三、方法3:使用 JVM 参数指定日志文件1、某些日志框架允许通过 JVM 参数来指定日志文件。例如,使用 Log4j 2,你可以在启动时通过系统属性来设置日志文件:1java -D log4j.configurationFile=path/to/log4j2.xml -jar example.jar2、其中 log4j2.xml 应该包含一个类似于上面 Logback 配置的配置,指定输出到特定文件。四、方法4:使用第三方库或工具对于一些复杂的场景,你可能会想要使用更高级的日志管理工具,如 Logrotate(在 Linux 上)或者使用第三方 Java 库如 log4j-layout-tpl 来实现更复杂的日志轮转和归档策略。例如,使用 Logrotate 可以自动管理日志文件的大小和轮转。1、示例:使用 Log4j2 的 XML 配置文件确保你的 example.jar 包含了 Log4j2 的依赖,并创建一个 log4j2.xml 文件在你的项目资源目录中(例如 src/main/resources),内容如下:12345678910111213<?xml version="1.0" encoding="UTF-8"?><Configuration>    <Appenders>        <File name="LogFile" fileName="path/to/your/logfile.log">            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>        </File>    </Appenders>    <Loggers>        <Root level="info">            <AppenderRef ref="LogFile"/>        </Root>    </Loggers></Configuration>2、然后,在运行你的 jar 时指定 Log4j2 的配置文件:1java -Dlog4j.configurationFile=path/to/log4j2.xml -jar example.jar3、这样,你的应用日志就会输出到指定的文件了。
  • [技术干货] Java使用Redis实现消息订阅/发布的几种方式【转载】
    Redis 提供了 Pub/Sub (发布/订阅) 模式,允许客户端订阅频道并接收发布到这些频道的消息。以下是 Java 中使用 Redis 实现消息订阅的几种方式。1. 使用 Jedis 客户端添加依赖12345<dependency>    <groupId>redis.clients</groupId>    <artifactId>jedis</artifactId>    <version>4.3.1</version></dependency>基本订阅示例12345678910111213141516171819202122232425import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPubSub;  public class RedisSubscriber {    public static void main(String[] args) {        // 创建 Jedis 连接        Jedis jedis = new Jedis("localhost", 6379);                 // 创建订阅者        JedisPubSub subscriber = new JedisPubSub() {            @Override            public void onMessage(String channel, String message) {                System.out.println("收到消息 - 频道: " + channel + ", 内容: " + message);            }                         @Override            public void onSubscribe(String channel, int subscribedChannels) {                System.out.println("订阅成功 - 频道: " + channel);            }        };                 // 订阅频道        jedis.subscribe(subscriber, "myChannel");    }}发布消息123456789import redis.clients.jedis.Jedis;  public class RedisPublisher {    public static void main(String[] args) {        Jedis jedis = new Jedis("localhost", 6379);        jedis.publish("myChannel", "Hello, Redis Pub/Sub!");        jedis.close();    }}2. 使用 Lettuce 客户端 (推荐)Lettuce 是另一个流行的 Redis Java 客户端,支持响应式编程。添加依赖12345<dependency>    <groupId>io.lettuce</groupId>    <artifactId>lettuce-core</artifactId>    <version>6.2.3.RELEASE</version></dependency>订阅示例12345678910111213141516171819202122232425262728293031323334353637383940414243import io.lettuce.core.RedisClient;import io.lettuce.core.pubsub.RedisPubSubListener;import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands;  public class LettuceSubscriber {    public static void main(String[] args) {        RedisClient client = RedisClient.create("redis://localhost");        StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub();                 connection.addListener(new RedisPubSubListener<String, String>() {            @Override            public void message(String channel, String message) {                System.out.println("收到消息 - 频道: " + channel + ", 内容: " + message);            }                         @Override            public void message(String pattern, String channel, String message) {                // 模式匹配的消息            }                         @Override            public void subscribed(String channel, long count) {                System.out.println("订阅成功 - 频道: " + channel);            }                         // 其他需要实现的方法...        });                 RedisPubSubCommands<String, String> sync = connection.sync();        sync.subscribe("myChannel");                 // 保持程序运行以持续接收消息        try {            Thread.sleep(Long.MAX_VALUE);        } catch (InterruptedException e) {            e.printStackTrace();        }                 connection.close();        client.shutdown();    }}3. Spring Data Redis 集成如果你使用 Spring Boot,可以更方便地集成 Redis Pub/Sub,这也是比较常用的方式添加依赖1234<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId></dependency>配置 Redis 容器123456789101112131415161718import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.listener.ChannelTopic;import org.springframework.data.redis.listener.RedisMessageListenerContainer;  @Configurationpublic class RedisConfig {         @Bean    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,                                                 RedisMessageSubscriber subscriber) {        RedisMessageListenerContainer container = new RedisMessageListenerContainer();        container.setConnectionFactory(connectionFactory);        container.addMessageListener(subscriber, new ChannelTopic("myChannel"));        return container;    }}配置订阅123456789101112import org.springframework.data.redis.connection.Message;import org.springframework.data.redis.connection.MessageListener;import org.springframework.stereotype.Component;  @Componentpublic class RedisMessageSubscriber implements MessageListener {      @Override    public void onMessage(Message message, byte[] pattern) {        System.out.println("收到消息: " + new String(message.getBody()));    }}发布消息12345678910111213141516import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;  @Servicepublic class RedisMessagePublisher {         private final RedisTemplate<String, Object> redisTemplate;         public RedisMessagePublisher(RedisTemplate<String, Object> redisTemplate) {        this.redisTemplate = redisTemplate;    }         public void publish(String message) {        redisTemplate.convertAndSend("myChannel", message);    }}高级功能模式匹配订阅:可以使用 psubscribe 订阅匹配模式的频道取消订阅处理连接断开:实现 onPMessage, onPUnsubscribe 等方法处理各种事件1234567891011//模式匹配订阅// Jedisjedis.psubscribe(subscriber, "news.*");  // Lettucesync.psubscribe("news.*");    //取消订阅subscriber.unsubscribe("myChannel");subscriber.punsubscribe("news.*");
  • [技术干货] Java 打印日志,字符串拼接报 NullPointerException 问题
    log.info("requestBody: " + finalBody); 这段代码在 finalBody 为 null 时会抛出 NullPointerException,因为 Java 中不能对 null 调用 String 的方法(如 + 拼接操作实际上会调用 String.valueOf(),但直接拼接 null 字符串是允许的,但这里可能涉及日志框架的特殊处理)。具体分析如果 log.info() 内部直接拼接字符串:"requestBody: " + finalBody 在 finalBody == null 时会返回字符串 "requestBody: null"(不会报错,因为 Java 的 + 操作符允许拼接 null)。但某些日志框架(如 SLF4J、Log4j)可能对 null 有特殊处理,如果 log.info() 内部尝试调用 finalBody.toString(),则会抛出 NullPointerException。如果 log.info() 使用 {} 占位符(推荐方式):log.info("requestBody: {}", finalBody); // 更安全,日志框架会自动处理 null 这种方式不会报错,日志框架(如 SLF4J)会将 null 转换为 "null" 字符串输出。如何避免 NullPointerException?方案 1:使用 {} 占位符(推荐)log.info("requestBody: {}", finalBody); // 自动处理 null 方案 2:手动判空log.info("requestBody: " + (finalBody != null ? finalBody : "null")); 方案 3:使用 String.valueOf()(避免 NPE)log.info("requestBody: " + String.valueOf(finalBody)); // null 会变成 "null" 结论如果 log.info() 直接拼接字符串(+ 操作符),通常不会报错,但取决于日志框架的实现。最安全的方式是使用 {} 占位符(如 SLF4J),它能正确处理 null 值。如果日志框架不支持 {} 占位符,建议手动判空或使用 String.valueOf()。示例(SLF4J/Logback)import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Example { private static final Logger log = LoggerFactory.getLogger(Example.class); public static void main(String[] args) { String finalBody = null; log.info("requestBody: {}", finalBody); // 输出: requestBody: null log.info("requestBody: " + finalBody); // 通常不会报错,但取决于日志框架 } } 推荐使用 {} 占位符,避免潜在的 NullPointerException。
总条数:691 到第
上滑加载中