-
前言本文主要介绍与Java中抽象类和接口相关的部分知识。一、抽象类在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。在打印图形例子中,我们发现,父类Shape中的draw方法好像并没有什么实际工作,主要的绘制图形都是由Shape的各种子类的draw方法来完成的。像这种没有实际工作的方法,我们可以把它设计成一个抽象方法(abstract method),包含抽象方法的类我们称为抽象类(abstract class)。在Java中,一个类如果被abstract修饰称为抽象类,抽象类中被abstract修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。// 抽象类:被abstract修饰的类public abstract class Shape {// 抽象方法:被abstract修饰的方法,没有方法体abstract public void draw();abstract void calcArea();// 抽象类也是类,也可以增加普通方法和属性public double getArea(){return area;}protected double area; // 面积}一键获取完整项目代码java需要注意的是,抽象类也是类,内部是可以包含普通方法和属性的,甚至构造方法也可以。同时,抽象类有着如下的特性,首先它无法直接实例化对象。我们可以键入如下的代码:package demo; public abstract class Shape { public abstract void draw(); abstract void calcArea(); public double getArea() { return area; } protected double area;} 一键获取完整项目代码javapackage demo; public class Test { public static void main(String[] args) { Shape shape = new Shape(); }}一键获取完整项目代码java其运行结果如下: 其次抽象方法是不能被private修饰的。比如下面的代码:package demo; public abstract class Shape { private abstract void draw(); abstract void calcArea(); public double getArea() { return area; } protected double area;} 一键获取完整项目代码java其运行结果如下: 再然后抽象方法无法被final和static修饰,因为其一定会被子类方法重写。我们键入如下的代码:package demo; public abstract class Shape { public abstract void draw(); //private abstract void draw(); abstract void calcArea(); public double getArea() { return area; } abstract final void methodA(); abstract public static void methodB(); protected double area;} 一键获取完整项目代码java其运行结果如下: 并且,抽象类必须被继承,在继承之后子类要重写父类中的抽象方法,否则子类也是抽象类,必须使用abstract修饰,比如如下的代码:package demo; public abstract class Shape { public abstract void draw(); //private abstract void draw(); abstract void calcArea(); public double getArea() { return area; } //abstract final void methodA(); //abstract public static void methodB(); protected double area;} 一键获取完整项目代码javapackage demo; public class Rect extends Shape{ private double length; private double width; Rect(double length, double width){ this.length = length; this.width = width; } public void draw(){ System.out.println("矩形: length= "+length+" width= " + width); } public void calcArea(){ area = length * width; }}一键获取完整项目代码javapackage demo; public class Circle extends Shape{ private double r; final private static double PI = 3.14; public Circle(double r){ this.r = r; } public void draw(){ System.out.println("圆:r = "+r); } public void calcArea(){ area = PI * r * r; }}一键获取完整项目代码javapackage demo; public abstract class Triangle extends Shape { private double a; private double b; private double c; @Override public void draw() { System.out.println("三角形:a = "+a + " b = "+b+" c = "+c); }}一键获取完整项目代码java我们这样是可以正常编译通过的。最后,抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类;抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量。那么我们怎么去认识到抽象类的作用呢?抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类。然后让子类重写抽象类中的抽象方法。有些读者可能会说了,普通的类也可以被继承,普通的方法也可以被重写,为啥非得用抽象类和抽象方法呢?确实如此,但是使用抽象类相当于多了一重编译器的校验,使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题。很多语法存在的意义都是为了预防出错,例如我们曾经用过的final也是类似,创建的变量用户不去修改,不就相当于常量嘛?但是加上final能够在不小心误修改的时候,让编译器及时提醒我们。充分利用编译器的校验,在实际开发中是非常有意义的。二、接口首先我们需要清楚接口究竟是什么?在现实生活中,接口的例子比比皆是,比如笔记本上的USB口,电源插座等。 电脑的USB口上,可以插U盘、鼠标、键盘……所有符合USB协议的设备;电源插座插孔上,可以插电脑、电视机、电饭煲……所有符合规范的设备。通过上述例子可以看出接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。在Java中,接口可以看成是多个类的公共规范,是一种引用数据类型。接口的定义格式与定义类的格式基本相同,将class关键字换成 interface关键字,就定义了一个接口。public interface 接口名称{// 抽象方法public abstract void method1(); // public abstract 是固定搭配,可以不写public void method2();abstract void method3();void method4();// 注意:在接口中上述写法都是抽象方法,更推荐方式4,代码更简洁}一键获取完整项目代码java创建接口时,接口的命名一般以大写字母I开头;接口的命名一般使用形容词词性的单词;阿里编码规范中约定,接口中的方法和属性不要加任何修饰符号,保持代码的简洁性。对于接口来说,它有着如下的特性,第一是接口类型虽然是一种引用类型,但是我们不能直接new接口的对象。我们键入如下的代码:package demo2; import demo.Shape; public interface IShape { public static void main(String[] args) { Shape shape = new Shape(); }}一键获取完整项目代码java其运行结果如下: 第二是接口中的每一个方法都是public的抽象方法,也就是说接口中的方法会被隐式指定为public abstract,并且只能是这个public abstract,其他修饰符就会报错。我们可以键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); public abstract void getArea( double a); private void calArea(double a); public static void main(String[] args) { }}一键获取完整项目代码java其运行结果如下: 第三是接口中的方法是不能在接口中实现的,只能由实现接口的类来实现。我们可以键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(){ System.out.println("draw()......."); } public static void main(String[] args) { }}一键获取完整项目代码java其运行结果如下: 第四是在重写接口中方法时,不能使用默认的访问权限,我们键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a);}一键获取完整项目代码javapackage demo2; public class Test implements IShape{ void draw(){ System.out.println("实现draw()......"); } void getArea(double a){ System.out.println("实现getArea(double a)......"); } public static void main(String[] args) { Test test = new Test(); test.draw(); }}一键获取完整项目代码java其运行结果如下: 第五是接口中可以含有变量,但是接口中的变量会被隐式指定为public static final变量,我们可以键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0;}一键获取完整项目代码javapackage demo2; public class Test{ public static void main(String[] args) { System.out.println(IShape.a); }}一键获取完整项目代码java其运行结果如下: 我们可以通过接口进行访问,说明a是具有static属性的。然后我们修改Test类的代码如下:package demo2; public class Test{ public static void main(String[] args) { System.out.println(IShape.a); IShape.a = 2.0; }}一键获取完整项目代码java其运行结果如下: 此时说明其具有final属性。第六则是接口中不能有静态代码块和构造方法,我们键入如下代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0; public IShape(){ }}一键获取完整项目代码java其运行结果如下: 再修改代码如下:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0; {}}一键获取完整项目代码java其运行结果如下: 第七是接口虽然不是类,但是接口编译完成后字节码文件的后缀也是.class;并且如果类中没有实现接口中所有的抽象方法,那么类就必须设置为抽象类。第八是接口中可以包含default方法,并且如果方法被default修饰,那么就可以有具体的实现。我们键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0; default void calArea() { System.out.println("calArea()......"); }}一键获取完整项目代码javapackage demo2; public class Test implements IShape{ public void draw(){ System.out.println("实现draw()......"); } public void getArea(double a){ System.out.println("实现getArea(double a)......"); } public static void main(String[] args) { Test test = new Test(); test.calArea(); }}一键获取完整项目代码java其运行结果如下: 最后第九是如果接口中的方法被static修饰,那么也可以有具体的实现,我们修改为如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0; static void calArea(){ System.out.println("calArea()......"); }}一键获取完整项目代码javapackage demo2; public class Test implements IShape{ public void draw(){ System.out.println("实现draw()......"); } public void getArea(double a){ System.out.println("实现getArea(double a)......"); } public static void main(String[] args) { Test test = new Test(); IShape.calArea(); }}一键获取完整项目代码java其运行结果如下: 刚刚我们在介绍接口特性的时候,相信读者已经发现,接口是不能直接使用的,我们必须有一个实现类来实现该接口,实现接口中的所有抽象方法。我们在上面是通过implements来实现的,其语法格式如下:public class 类名称 implements 接口名称{// ...}一键获取完整项目代码java需要注意的是,子类和父类之间是extends继承关系,类与接口之间是implements实现关系。我们可以看看这个实现笔记本电脑使用USB鼠标、USB键盘的例子,USB接口包含打开设备、关闭设备功能;笔记本类包含开机功能、关机功能、使用USB设备功能;鼠标类实现USB接口,并具备点击功能;键盘类实现USB接口,并具备输入功能。// USB接口public interface USB {void openDevice();void closeDevice();}// 鼠标类,实现USB接口public class Mouse implements USB {@Overridepublic void openDevice() {System.out.println("打开鼠标");}@Overridepublic void closeDevice() {System.out.println("关闭鼠标");}public void click(){System.out.println("鼠标点击");}}// 键盘类,实现USB接口public class KeyBoard implements USB {@Overridepublic void openDevice() {System.out.println("打开键盘");}@Overridepublic void closeDevice() {System.out.println("关闭键盘");}public void inPut(){System.out.println("键盘输入");}}// 笔记本类:使用USB设备public class Computer {public void powerOn(){System.out.println("打开笔记本电脑");}public void powerOff(){System.out.println("关闭笔记本电脑");}public void useDevice(USB usb){usb.openDevice();if(usb instanceof Mouse){Mouse mouse = (Mouse)usb;mouse.click();}else if(usb instanceof KeyBoard){KeyBoard keyBoard = (KeyBoard)usb;keyBoard.inPut();}usb.closeDevice();}}// 测试类:public class TestUSB {public static void main(String[] args) {Computer computer = new Computer();computer.powerOn();// 使用鼠标设备computer.useDevice(new Mouse());// 使用键盘设备computer.useDevice(new KeyBoard());computer.powerOff();}}一键获取完整项目代码java在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。下面通过类来表示一组动物:package demo3; public abstract class Animal { public String name; public int age; public Animal(String name, int age){ this.name = name; this.age = age; } public abstract void eat();}一键获取完整项目代码java在实现动物的时候,我们额外为它们创建对应的动作,跑、飞和游泳,但是不是说所有的动物都能实现这三种动作,所以说我们没法创建多个类来继承实现,但是我们可以创建多个接口来实现这三个动作。也就是如下的代码:package demo3; public interface IRunning { void run();}一键获取完整项目代码javapackage demo3; public interface IFlying { void fly();}一键获取完整项目代码javapackage demo3; public interface ISwimming { void swim();}一键获取完整项目代码java此时我们就可以创建一个鱼类,因为鱼只会游泳,所以他只能用ISwimming这个接口,也就是如下的代码:package demo3; public class Fish extends Animal implements ISwimming{ public Fish(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Fish.eat()......"); } public void swim(){ System.out.println("Fish.swim()......"); }}一键获取完整项目代码java需要注意的是,我们必须先继承父类再调用接口,这个顺序是不可以改变的。我们再来实现一个狗类,因为狗是既会跑,又会游泳的,所以有下面的代码:package demo3; public class Dog extends Animal implements IRunning, ISwimming{ public Dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Dog.eat()......"); } public void bark() { System.out.println("Dog.bark()......"); } @Override public void run() { System.out.println("Dog.run()......"); } @Override public void swim() { System.out.println("Dog.swim()......"); }}一键获取完整项目代码java所以我们接口的出现成功解决了Java无法多继承的问题。最后我们来创建一个鸭类,代码如下:package demo3; public class Duck extends Animal implements IRunning, ISwimming, IFlying{ public Duck(String name, int age){ super(name,age); } public void eat(){ System.out.println("Duck.eat()......"); } @Override public void fly() { System.out.println("Duck.fly()......"); } @Override public void run() { System.out.println("Duck.run()......"); } @Override public void swim() { System.out.println("Duck.swim()......"); }}一键获取完整项目代码java我们再来调用一下这些方法:package demo3; public class Test { public static void main(String[] args) { func1(new Duck("ducky", 3)); func1(new Dog("woofy", 2)); func1(new Fish("poofy", 1)); run(new Duck("ducky", 3)); swim(new Duck("ducky", 3)); fly(new Duck("ducky", 3)); run(new Dog("woofy", 2)); swim(new Dog("woofy", 2)); swim(new Fish("poofy", 1)); } public static void func1(Animal animal){ animal.eat(); } public static void run(IRunning irun){ irun.run(); } public static void fly(IFlying ifly){ ifly.fly(); } public static void swim(ISwimming iswim){ iswim.swim(); }}一键获取完整项目代码java其运行结果如下: 在完成上面的知识掌握后,我们可以来实现一下接口的继承。在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即用接口可以达到多继承的目的。接口可以使用extends关键字继承一个接口,达到复用的效果。interface IRunning {void run();}interface ISwimming {void swim();}// 两栖的动物, 既能跑, 也能游interface IAmphibious extends IRunning, ISwimming {}class Frog implements IAmphibious {...}一键获取完整项目代码java通过接口继承创建一个新的接口IAmphibious表示两栖的,此时实现接口创建的Frog类,就继续要实现run方法,也需要实现swim方法。接口间的继承相当于把多个接口合并在一起。三、Object类Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。比如说如下的代码:class Person{}class Student{}public class Test {public static void main(String[] args) {function(new Person());function(new Student());}public static void function(Object obj) {System.out.println(obj);}}一键获取完整项目代码java在运行过后,它就会打印出如下的结果:Person@1b6d3586Student@4554617c一键获取完整项目代码java所以在开发之中,Object类是参数的最高统一类型。但是Object类也存在有定义好的一些方法。如下这些: 对于整个Object类中的方法需要实现全部掌握。本文中我们主要来熟悉这几个方法,toString()方法,equals()方法,hashcode()方法。如果要打印对象中的内容,可以直接重写Object类中的toString()方法,之前已经讲过了,此处不再赘述。Object类中的toString()方法实现如下:public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());}一键获取完整项目代码java在Java中,==进行比较时:如果==左右两侧是基本类型变量,比较的是变量中值是否相同;如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同;如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的。在Object中的equals()方法如下:public boolean equals(Object obj) {return (this == obj); // 使用引用中的地址直接来进行比较}一键获取完整项目代码java我们借用一下之前实现的类,然后对代码作出如下的改动:package demo3; public class Test { public static void main(String[] args) { Dog dog1 = new Dog("woofy", 10); Dog dog2 = new Dog("woofy", 10); System.out.println(dog1 == dog2); System.out.println(dog1.equals(dog2)); }}一键获取完整项目代码java其运行结果如下: 我们此时可以通过重写这个equals()方法来实现我们想要的功能:package demo3; public class Dog extends Animal implements IRunning, ISwimming{ public Dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Dog.eat()......"); } public void bark() { System.out.println("Dog.bark()......"); } @Override public void run() { System.out.println("Dog.run()......"); } @Override public void swim() { System.out.println("Dog.swim()......"); } public boolean equals(Object obj){ Dog dog = (Dog)obj; return this.name.equals(dog.name) && this.age == dog.age; }}一键获取完整项目代码java这时候再去运行这个代码,我们就能得到如下的结果: 所以如果说我们要比较对象中的内容是否相同,我们就一定要重写equals方法。然后我们再来看一下它的hashCode()方法。它在Object类中的定义如下:@IntrinsicCandidate public native int hashCode();一键获取完整项目代码java在它的属性中,我们看到了一个native关键字,这就说明它本身是由C或C++代码实现的,我们无法看到它的详细实现过程。我们首先键入如下的代码:package demo3; public class Test { public static void main(String[] args) { Dog dog1 = new Dog("woofy", 10); Dog dog2 = new Dog("woofy", 10); System.out.println(dog1.hashCode()); System.out.println(dog2.hashCode()); } public static void func1(Animal animal){ animal.eat(); } public static void run(IRunning irun){ irun.run(); } public static void fly(IFlying ifly){ ifly.fly(); } public static void swim(ISwimming iswim) { iswim.swim(); }}一键获取完整项目代码java其运行结果如下: 我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,所以二者的哈希值应该相同。我们可以和前面的方法一样,对这个方法进行重写,即如下的代码:package demo3; import java.util.Objects; public class Dog extends Animal implements IRunning, ISwimming{ public Dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Dog.eat()......"); } public void bark() { System.out.println("Dog.bark()......"); } @Override public void run() { System.out.println("Dog.run()......"); } @Override public void swim() { System.out.println("Dog.swim()......"); } public boolean equals(Object obj){ Dog dog = (Dog)obj; return this.name.equals(dog.name) && this.age == dog.age; } @Override public int hashCode() { return Objects.hash(name, age); }}一键获取完整项目代码java这时我们再次运行我们的代码,会有如下的结果: 所以说,我们可以知道hashcode方法用来确定对象在内存中存储的位置是否相同。这个方法会帮我们算了一个具体的对象位置,这里面涉及数据结构,但是我们还没讲解Java实现数据结构,没法讲述,所以我们只能说它是个内存地址。然后调用Integer.toHexString()方法,将这个地址以16进制输出。事实上hashCode()在散列表中才有用,在其它情况下没用。在散列表中hashCode()的作用是获取对象的散列码,进而确定该对象在散列表中的位置。四、内部类在外部类中,内部类定义位置与外部类成员所处的位置相同,因此称为成员内部类。首先我们先来看看静态内部类,也就是被static修饰的成员内部类。我们键入如下的代码就可以完成对其的定义:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; static class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println("test方法执行了"); } }} public class Test {}一键获取完整项目代码java那么我们该如何实现对其的实例化呢?我们键入如下的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; static class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass.InnerClass innerClass = new OutClass.InnerClass(); innerClass.test(); }}一键获取完整项目代码java其运行结果如下: 但是需要注意,我们在静态类内部是无法直接访问外部类的非静态成员的,如果一定要访问的话,我们就必须先先完成它的实例化。比如说如下的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; static class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ OutClass outClass = new OutClass(); System.out.println(outClass.data1); System.out.println(outClass.data2); System.out.println(outClass.data3); System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass.InnerClass innerClass = new OutClass.InnerClass(); innerClass.test(); }}一键获取完整项目代码java其运行结果如下: 我们额外还需要注意创建静态内部类对象时,不需要先创建外部类对象。然后是实例内部类对象,我们键入如下的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { }}一键获取完整项目代码java那么我们该如何实例化一个实例内部类对象呢?我们键入如下的方法:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass outClass = new OutClass(); OutClass.InnerClass innerclass = outClass.new InnerClass(); innerclass.test(); }}一键获取完整项目代码java其运行结果如下: 而如果说我们再添加打印数据的代码时,能否正常的进行运行呢?我们键入如下的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println(data1); System.out.println(data2); System.out.println(data3); System.out.println(data4); System.out.println(data5); System.out.println(data6); System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass outClass = new OutClass(); OutClass.InnerClass innerclass = outClass.new InnerClass(); innerclass.test(); }}一键获取完整项目代码java其运行的结果如下: 如果说我们此时内部类和外部类中有着同名的成员变量的话,我们该如何去区分和访问呢?这时可以看下我们下面的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; class InnerClass{ public int data1 = 1000; public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println(data1); System.out.println(this.data1); System.out.println(OutClass.this.data1); System.out.println(data2); System.out.println(data3); System.out.println(data4); System.out.println(data5); System.out.println(data6); System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass outClass = new OutClass(); OutClass.InnerClass innerclass = outClass.new InnerClass(); innerclass.test(); }}一键获取完整项目代码java此时其运行结果如下: 所以我们可以作出如下的总结,外部类中的任何成员都可以在实例内部类方法中直接访问;实例内部类所处的位置与外部类成员位置相同,因此也受public、private等访问限定符的约束;在实例内部类方法中访问同名的成员时,优先访问自己的,如果要访问外部类同名的成员,必须通过外部类名称.this.同名成员的语法格式来访问;实例内部类对象必须在先有外部类对象前提下才能创建;实例内部类的非静态方法中包含了一个指向外部类对象的引用;外部类中,不能直接访问实例内部类中的成员,如果要访问必须先要创建内部类的对象。然后我们再来看看局部内部类,它是定义在外部类的方法体或者{}中,该种内部类只能在其定义的位置使用,一般使用的非常少,此处简单了解下语法格式。我们键入如下的代码:package demo4; public class Test { public void testMethod(){ class Inner{ public int data1; public void func(){ System.out.println("func"); } } Inner inner = new Inner(); inner.func(); } public static void main(String[] args) { Test test = new Test(); test.testMethod(); }}一键获取完整项目代码java其运行结果如下: 需要注意的是,局部内部类只能在所定义的方法体内部使用,不能被public、static等修饰符修饰;编译器也有自己独立的字节码文件,命名格式为外部类名字$数字内部类名字.class,这种类几乎不会使用。最后是我们的匿名内部类,我们直接键入如下的代码:package demo4; class out{ public void test(){ System.out.println("test1......"); }} public class Test { public static void main(String[] args) { new out(){ public void test(){ System.out.println("test2......"); } }.test(); }}一键获取完整项目代码java其运行结果如下: 当然这种方式也可以在接口中实现。我们键入如下的代码:package demo4; interface IA{ void test();} public class Test { public static void main(String[] args) { new IA(){ public void test(){ System.out.println("test1......"); } }.test(); }}一键获取完整项目代码java其运行结果如下: 五、接口使用实例首先我们要介绍的接口就是Compareable接口,我们现在看看下面一段代码:package demo5; class Student{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }} public class Test { public static void main(String[] args) { Student student1 = new Student("zhangsan", 18); Student student2 = new Student("lisi", 19); System.out.println(student1 < student2); }}一键获取完整项目代码java这段代码是无法正常执行的,因为我们最下面这行代码会产生报错,student1和student2这两个对象是无法通过简单的逻辑运算符去比较的。我们目前遇到的问题总结来说就是当前的自定义类,到底要按照什么样的规则去进行比较,并且这个比较的规则该如何去定义?我们首先先在这个自定类型后面添加一个implements Comparable,然后我们转到这个Compareable的声明部分:public interface Comparable<T> { public int compareTo(T o);}一键获取完整项目代码java我们按照它的形式进行修改,也就是在它的后面再添加一个<Student>即可,这个<>中的部分就是我们想要去比较的类型,也就是说我们想去比较哪个类就去写哪个类,它叫做泛型,我们之后会去详细讲解。我们可以将代码改为如下:package demo5; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.age - o.age; }} public class Test { public static void main(String[] args) { Student student1 = new Student("zhangsan", 18); Student student2 = new Student("lisi", 19); System.out.println(student1.compareTo(student2)); }}一键获取完整项目代码java再次运行,得到结果如下: 但是这种比较有一种相对大的缺陷,就是它一般用于固定的比较,不适合非常灵活的比较。比如说我们现在不想根据年龄比较了,而是根据姓名去比较的话,就会发生一些变化。我们可以去看一下String类中是否也有compareTo的声明,在查询后,我们可以看到的确是有的: public int compareTo(String anotherString) { byte v1[] = value; byte v2[] = anotherString.value; byte coder = coder(); if (coder == anotherString.coder()) { return coder == LATIN1 ? StringLatin1.compareTo(v1, v2) : StringUTF16.compareTo(v1, v2); } return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2) : StringUTF16.compareToLatin1(v1, v2); }一键获取完整项目代码java那我们直接更改一下我们刚才的方法中的return语句,代码如下:package demo5; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.name.compareTo(o.name); }} public class Test { public static void main(String[] args) { Student student1 = new Student("zhangsan", 18); Student student2 = new Student("lisi", 19); System.out.println(student1.compareTo(student2)); }}一键获取完整项目代码java我们此时再度运行代码,发现其结果变为如下: 这就说明如果按照姓名去比的话,我们的student1是要比student2要大的。所以就说明了我们刚刚提到的缺陷。通常来说,我们会在默认的比较上使用这种方式。那我们如果想要解决这个问题的话,就需要换一个接口来解决。我们现在来改变一下原来的问题,如果说我们现在不想只比较两个学生,而是多个学生的话,我们该如何操作?我们很容易的想到就是通过构建一个Student类型的数组,也就是如下的代码:package demo5; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.name.compareTo(o.name); }} public class Test { public static void main(String[] args) { Student[] students = new Student[3]; students[0] = new Student("zhangsan", 18); students[1] = new Student("lisi", 19); students[2] = new Student("wangwu", 17); }}一键获取完整项目代码java然后我们又会想到在数组中我们有sort方法可以进行比较,那么能不能就直接通过sort方法实现它们的排序呢?我们键入如下的代码尝试实现:package demo5; import java.util.Arrays; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.name.compareTo(o.name); }} public class Test { public static void main(String[] args) { Student[] students = new Student[3]; students[0] = new Student("zhangsan", 18); students[1] = new Student("lisi", 19); students[2] = new Student("wangwu", 17); Arrays.sort(students); System.out.println(Arrays.toString(students)); }}一键获取完整项目代码java其运行结果如下:可以看到它这个是成功为我们实现了排序。但是实际上我们是因为实现了compareTo方法的重写和Comparable方法,才成功的,如果我们将这两部分注释掉,就会出现下面的情况: 我们可以看到这里的报错是ClassCastException,这个叫做类型转换异常。我们可以看看他后面的信息:class demo5.Student cannot be cast to class java.lang.Comparable一键获取完整项目代码java这就是说我们的这个Student类没办法转换为Comparable的形式,我们点击下面的那个ComparableTimSort进去看看:if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0) runHi++; reverseRange(a, lo, runHi); } else { // Ascending while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0) runHi++; }一键获取完整项目代码java可以看到它这个是要把我们的Student强转成Comparable,然后去调用compareTo的,但是由于我们注释掉了代码,就没办法实现了。但在调用之前,我们必须先重写我们的compareTo,要不然还是会报错。当然我们也可以自己写一个排序的代码: package demo5; import java.util.Arrays; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.name.compareTo(o.name); }} public class Test { public static void main(String[] args) { Student[] students = new Student[3]; students[0] = new Student("zhangsan", 18); students[1] = new Student("lisi", 19); students[2] = new Student("wangwu", 17); //Arrays.sort(students); mySort(students); System.out.println(Arrays.toString(students)); } public static void mySort(Comparable[] array) { for (int bound = 0; bound < array.length; bound++) { for (int cur = array.length - 1; cur > bound; cur--) { if (array[cur - 1].compareTo(array[cur]) > 0) { Comparable tmp = array[cur - 1]; array[cur - 1] = array[cur]; array[cur] = tmp; } } } }}一键获取完整项目代码java其运行结果如下: Java中还内置了一个十分有用的接口,Object类中存在一个clone方法,调用这个方法可以创建一个对象的拷贝。但是要想合法调用clone方法, 必须要先实现Clonable接口, 否则就会抛出 CloneNotSupportedException异常,我们进入查看这个Cloneable接口:public interface Cloneable {}一键获取完整项目代码java发现这是一个空接口,它也叫标记接口,代表着当前类是可以被扩容的。但是它还是会报错,这涉及到了我们之后会提到的异常处理机制,我们之后会详细说明,我们只需要将代码改为如下的部分即可:package demo6; public class Person implements Cloneable{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}一键获取完整项目代码javapackage demo6; public class Test { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("zhangsan", 10); Person person2 = (Person) person1.clone(); }}一键获取完整项目代码java总的来说是走了如下的过程:我们修改代码如下:package demo6; class Money{ public double money = 9.9;} public class Person implements Cloneable{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public Money m = new Money(); @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}一键获取完整项目代码javapackage demo6; public class Test { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("zhangsan", 10); Person person2 = (Person) person1.clone(); person2.m.money = 99.99; System.out.println(person1.m.money); System.out.println(person2.m.money); }}一键获取完整项目代码java其运行结果如下: 可以看到二者的钱都发生了改变,这是为什么?原因就如下图所示: 也就是说我们只克隆了人这个对象,却没有克隆钱这个对象。目前这种拷贝就叫做浅拷贝,预支对应的就是深拷贝,也就是说我们把钱这个对象也实现了克隆。要实现深拷贝,我们就需要在过程中完成对于钱的克隆,那么我们就可以修改代码如下:package demo6; class Money implements Cloneable { public double money = 9.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }} public class Person implements Cloneable{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public Money m = new Money(); @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { Person tmp = (Person) super.clone(); tmp.m = (Money) this.m.clone(); return tmp; }}一键获取完整项目代码java再次执行,其结果如下:六、抽象类和接口的区别抽象类和接口都是 Java 中多态的常见使用方式。都需要重点掌握,同时又要认清两者的区别,其核心区别为抽象类中可以包含普通方法和普通字段,这样的普通方法和字段可以被子类直接使用(不必重写),而接口中不能包含普通方法,子类必须重写所有的抽象方法。如之前写的Animal例子,此处的Animal中包含一个name这样的属性,这个属性在任何子类中都是存在的。因此此处的Animal只能作为一个抽象类,而不应该成为一个接口。对于二者具体的区分我们可以总结为如下的这个表格:总结本文系统介绍了Java中抽象类和接口的核心概念与应用。抽象类通过abstract修饰,包含抽象方法(无实现)和普通方法/属性,不能实例化,必须被子类继承并实现抽象方法。接口定义公共规范标准,所有方法默认为public abstract,支持多继承,可通过implements实现。文章详细对比了两者特性,并通过图形绘制、USB设备等实例演示实际应用。同时介绍了Object类的重要方法(toString、equals、hashCode)、内部类(静态/实例/局部/匿名)及常用接口(Comparable、Cloneable),最后强调抽象类与接口的核心区别在于抽象类可包含普通实现而接口只能定义规范。————————————————原文链接:https://blog.csdn.net/su17643217590/article/details/155354247
-
一、finally的使用说明1、finally的理解1、我们将一定要被执行的代码声明在finally结构中2、更深刻的理解:无论try中或者catch中是否存在仍未被处理的异常,无论try中或catch中是否存在return语句等,finally中声明的语句都一定要被执行。3、finally语句和catch语句是可选的,但是finally不能单独使用4、try-catch可以嵌套使用 2、什么样的代码我们一定要声明在finally中呢?我们在开发中,有一些资源,比如:(输入流、输出流、数据库连接、socket连接等资源),在使用完以后,必须显式的进行关闭操作,否则,GC不会自动的回收这些资源。进而导致内存泄漏。为了保证这些资源在使用完之后,不管是否出现了未被处理的异常的情况下,这些资源能被关闭。我们必须将这些操作声明在finally中 package trycatchfinally; import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException; /** * package:trycatchfinally * * @Author jimmy-yan * @Create 2024/11/19 11:59 */public class FinallyTest { public static void main(String[] args) { FinallyTest f = new FinallyTest(); f.test1(); } public void test1() { try { String str = "123"; str = "abc"; int i = Integer.parseInt(str); System.out.println(i); } catch (NumberFormatException e) { e.printStackTrace(); System.out.println(10 / 0); //在catch中抛出异常 } finally { System.out.println("程序执行结束"); } } public void test2() { FileInputStream fis = null; try { File file = new File("D:\\hello.txt"); fis = new FileInputStream(file); //可能报FileFonudException int data = fis.read(); //可能报IOException while (data != -1) { System.out.println((char) data); data = fis.read(); //可能报IOException } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fis != null) { fis.close(); //可能报IOException } } catch (IOException e) { e.printStackTrace(); } } }} 一键获取完整项目代码java 二、异步处理的方式二:throws1、格式在方法的声明处,使用throws异常类型1,异常类型2,… 2、举例public void test() throws 异常类型1,异常类型2,..{//可能存在编译时异常}一键获取完整项目代码java 3、是否真正处理了异常?从编译是否能通过的角度看:看成是给出了异常万一要是出现时候的解决方案。此方案就是,继续向上抛出(throws)。但是,此throws的方式,仅是将可能出现的异常抛给了此方法的调用者,此调用者仍然需要考虑如何处理相关异常。从这个角度来看,throws的方式不算真正意义上处理了异常。 import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException; /** * package:PACKAGE_NAME * * @Author jimmy-yan * @Create 2024/11/19 13:58 */public class ThrowsTest { public static void main(String[] args) { test3(); } public static void test3() { try{ test2(); }catch (FileNotFoundException e){ e.printStackTrace(); }catch (IOException e){ e.printStackTrace(); } } public static void test2() throws FileNotFoundException, IOException { File file = new File("D:\\hello.txt"); FileInputStream fis = new FileInputStream(file); //可能报FileFonudException int data = fis.read(); //可能报IOException while (data != -1) { System.out.println((char) data); data = fis.read(); //可能报IOException } fis.close(); //可能报IOException }} 一键获取完整项目代码java 4、方法重写的要求(只针对编译型异常)子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或者是父类被重写的方法抛出异常类型的子类。 5、开发中,如何选择异常处理的两种方式?1、如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用try-catch-finally来处理,保证不出现内存泄漏。2、如果父类被重写的方法没有throws异常类型,则子类重写的方法中如果出现异常,只能考虑使用try-catch-finally进行处理,不能throws。3、开发中,方法a中依次调用了方法b,c,d等方法,方法b,c,d之间是递进关系。此时,如果方法b,c,d中有异常,我们通常选择使用throws,而方法a中通常选择使用try-catch-finally。 三、使用throw手动抛出异常对象1、为什么需要手动抛出异常在实际开发中,如果出现不满足具体场景的代码问题,我们就有必要手动抛出一个指定类型的异常对象。 2、如何理解 自动vs手动 抛出异常过程1:“抛”“自动抛”:程序在执行的过程当中,一旦出现异常,就会在出现异常的代码处,自动生成对应异常类的对象,并将此对象抛出。“手动抛”:程序在执行的过程当中,不满足指定条件的情况下,我们主动的使用"throw + 异常类的对象"方式抛出异常对象。 过程2:“抓”狭义上讲:try-catch的方式捕获异常,并处理。广义上讲:把“抓"理解为"处理"。则此时对应着异常处理的两种方式: try-catch-finathrows3、如何实现手动抛出异常在方法内部,满足指定条件的情况下,使用“throw 异常类的对象”的方式抛出。 /** * package:PACKAGE_NAME * * @Author jimmy-yan * @Create 2024/11/19 14:51 */public class Throw { public static void main(String[] args) { Throw t = new Throw(); t.regist(-9); System.out.println(t); } int id; public void regist(int id) { if (id > 0) { this.id = id; } else {// System.out.println("输入的非法id"); //todo 手动抛出异常 throw new RuntimeException("输入的id非法"); } } @Override public String toString() { return "Throw{" + "id=" + id + '}'; }} 一键获取完整项目代码java 注意一点:throws后的代码不能被执行,编译不通过————————————————原文链接:https://blog.csdn.net/YZL40514131/article/details/143879989
-
一、体会线程安全问题当我们编写一个多线程程序,要求两个线程对同一个变量(共享变量)进行修改,得到的结果是否与预期一致?创建两个线程,分别对共享变量(count)进行自增5万次操作,最后输出的结果理论上应为10万,但是实际上输出的结果是一个小于10万且不确定的数。读者可以自行实现一下该多线程程序,运行后看看结果是否符合预期。public class Demo14_threadSafety { private static int count = 0; public static void main1(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); // 理论上输出的结果应是100000,实际输出的结果是0 // 原因是主线程 main 运行太快了,当 t1 和 t2 线程还在计算时,主线程已经打印结果、运行完毕了 System.out.println(count); } // 让主线程等待 t1 和 t2 线程,等到它们两个都执行完成再打印,故使用 join 方法 public static void main2(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); // 在主线程中,通过 t1 和 t2 对象调用 join 方法 // 表示让主线程 main 等待 t1 线程和 t2 线程 t1.join(); t2.join(); // 当两个线程都执行完毕后主线程再继续执行打印操作 System.out.println(count); // 实际输出的结果小于100000,仍不符合预期 }}一键获取完整项目代码java二、线程安全的概念通过上面的一个例子,想必读者已经体会到线程安全问题了吧?那究竟什么是线程安全问题呢?其原因是什么?如何解决线程安全问题呢?不要急,且听小编慢慢道来~如果在多线程环境下运行的程序其结果符合预期或与在单线程环境下运行的结果一致,就说这个程序是线程安全的,否则是线程不安全的。上面的例子在单线程环境下运行——比如来两个循环对共享变量进行自增操作,那么结果是符合预期的;但是在多线程环境下运行就不符合预期。因此该程序是线程不安全的,也可以说该程序存在线程安全问题。三、线程安全问题的原因究竟是哪里出问题导致程序出现线程安全问题呢?究其根本,罪魁祸首是 操作系统的线程调度有随机性/抢占式执行 。由于操作系统的线程调度是有随机性的,这就会存在这种情况:某一个线程还没执行完呢,就调度到其他线程去执行了,从而导致数据不正确。当然了,一个巴掌拍不响,还有以下三个导致线程不安全的原因:原子性:指 Java 语句,一条 Java 语句可能对应不止一条指令,若对应一条指令,就是原子的。可见性:一个线程对主内存(共享变量)的修改,可以及时被其他线程看到。有序性:一个线程观察其他线程中指令的执行顺序,由于 JVM 对指令进行了重排序,观察到的顺序一般比较杂乱。因其原理与 CPU 及编译器的底层原理有关,暂不讨论。之前的例子就是由于原子性没有得到保障而出现线程安全问题:public static void main2(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count);}一键获取完整项目代码java1. “count ++” 这条语句对应多条指令:读取数据、计算结果、存储数据。2. t1 线程和 t2 线程分别执行一次“count++”语句,期望的结果是“count = 2”,其过程如下: 初始情况: 当线程执行“count ++”时,总共分三个步骤:load、update、save,由于线程调度的随机性/抢占式执行,可能会出现以下情况(可能出现的情况有很多种,这里只是其中一种): 这时候 t1 正在执行“count ++”这条语句,执行了“load 和 update”指令后,t1 的工作内存(寄存器)存着更新后的值,但是还未被写回内存中: 接着调度到 t2 线程并开始执行“count ++”语句,并且语句中包含的三条指令都执行。此时由于 t1 更新后的 count 的值还未写回内存,因此 t2 执行 load 操作所获取到的 count 仍是 0。接着 t2 执行 update 和 save 指令: 当 t2 执行完成,内存的 count 已被修改为 1 。此时调度回 t1 线程并继续执行 save 指令,但是 t1 线程寄存器中 count 的值也是 1 ,此时写回内存更新后 count 的值依然是 1 。 结果 count = 1,与预期的 count = 2 不符,因此存在线程安全问题,其原因是操作系统的随机线程调度和 count 语句存在非原子性。四、解决线程安全问题的方法从上面的例子我们知道,当一条语句的指令被拆开来执行的话是存在线程安全问题的,但是,当我们将“count ++”这条语句的三个指令都放在一起执行怎么样? 当线程调度的情况如下: 此时 t1 线程开始执行“count ++”语句的 load、update 和 save 指令。内存中的 count 为 0,t1 读取到内存中的 count 之后更新至 1 并写回内存中。当 t1 执行完成后内存的 count 由 0 更新至 1: 接着调度至 t2 线程,开始执行“count ++”语句的 load、update 和 save 指令。经过更新后内存中的 count 为 1,此时 t2 读取 count 并更新为 2,然后写回内存中。当 t2 执行完成,内存中的 count 就更新成 2 了: 可以发现,结果与预期相符!说明这个方法可行。可以将操作顺序改成先让 t1 线程完成“count ++”操作,再让 t2 线程完成该操作——即串行执行。现在我们对之前的例子进行优化:// 可以试着让 t1 线程先执行完后,再让 t2 线程执行,改成串行执行public static void main3(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t1.join(); t2.start(); t2.join(); System.out.println(count);}一键获取完整项目代码java刚刚是让一个线程一次性执行“count ++”这条语句的三个指令,也就是说,我们是通过这样操作将原本是非原子的三条指令打包成了一个原子指令(即执行过程中不可被打断——调度走)。这样就有效的解决了线程安全问题。而上述的操作,其实就是 Java 中的 加锁操作 。当一个线程执行一个非原子的语句时,通过加锁操作可以防止在执行过程中被调度走或被其他线程打断,若其他线程想要执行该语句,则要进入 阻塞等待 的状态,当线程执行完毕并将锁释放,操作系统这时唤醒等待中的线程,才可以执行该语句。就相当于上厕所:当厕所内没有人时(没有线程加锁),就可以使用;当厕所内有人时(已经有线程加锁了),那么就必须等里面的人出来后才能使用。注意:前一个线程解锁之后,并不是后一个线程立刻获取到锁。而是需要靠操作系统唤醒阻塞等待中的线程的。若 t1、t2 和 t3 三个线程竞争同一个锁,当 t1 线程获取到锁,t2 线程再尝试获取锁,接着 t3 线程尝试获取锁,此时 t2 和 t3 线程都因获取锁失败而处于阻塞等待状态。当 t1 线程释放锁之后,t2 线程并不会因为先进入阻塞状态在被唤醒后比 t3 先拿到锁,而是和 t3 进行公平竞争。(不遵循先来后到原则)4.1 synchronized 关键字在处理由原子性导致的线程安全问题时,通常采用加锁操作。加锁 / 解锁这些操作本身是在操作系统所提供的 API 中的,很多编程语言对其进行了封装,Java 中使用 synchronized 关键字来进行加锁 / 解锁操作,其底层是使用操作系统的 mutex lock 来实现的。Java 中的任何一个对象都可以用作“锁”。synchronized (锁对象){ ——> 进入代码块,相当于加锁操作 // 一些需要保护的逻辑} ——> 出了代码块,相当于解锁操作当多个线程针对同一个锁对象竞争的时候,加锁操作才有意义。对之前的例子进行加锁操作:public class Demo15_synchronized { private static int count = 0; public static void main1(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } System.out.println("t2-结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); }}一键获取完整项目代码javasynchronized 关键字用来修饰 普通方法 时,相当于给 this 加锁;synchronized 关键字用来修饰 静态方法 时,相当于给 类对象 加锁。于是可以使用另一种写法:// 写法二:// 将 count++ 所包含的三个操作封装成一个 add 方法// 使用 synchronized 修饰 add 方法 class Counter { private int count = 0; synchronized public void add () { // synchronized 修饰普通方法相当于给 this 加锁 count++; } // 相当于: // public void add () { // synchronized (this) { // count++; // } // } public int get () { return count; }} public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.get());}一键获取完整项目代码java这样一来,就成功解决了多线程程序中由原子性导致的线程安全问题。4.2 volatile 关键字我们再来一个例子:让 t1 线程读取共享变量的值,然后让 t2 线程修改共享变量的值,我们可以写出如下程序:public class Demo17_volatile { private static int flag = 0; public static void main1(String[] args) { Thread t1 = new Thread(() -> { while (flag == 0) { // 当 flag 为 0 时一直循环 } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag 的值:"); flag = in.nextInt(); }); t1.start(); t2.start(); // 运行后发现即使输入1,t1 线程并不会结束 }}一键获取完整项目代码java我们期望该程序运行后,输入非零的数如 1,t1 线程能够结束,实际并非如此,很显然,这也是出现了线程安全问题。这次出现问题的原因并非原子性,而是 可见性 。我们说过,可见性是一个线程对主内存(共享变量)的修改能够被其他线程及时看到,若不能被其他线程及时看到,就会出现数据错误,从而导致线程安全问题。如果我们使用锁来处理的话,好像并不能解决。我们先来认识一个东西:编译器优化。由于不能保证写代码的人每一次写代码都不会出错,所以开发 JDK 的先人们就让编译器 / JVM 能够根据代码的原有逻辑进行优化。在我们这个程序中:while {...load...cmp...} 先将数据从内存中 load 到寄存器中,然后在寄存器进行 “条件比较” 指令 cmp 。在短时间内用户还没来得及输入呢,但是这个循环语句能够执行成万上亿次且内存中的 flag 的值一直都是 0。那么这时候编译器 / JVM 就察觉到:既然一直读取 flag 都是同一个值,那干脆我直接读取寄存器算了(读取寄存器开销更小)。这样一来,当用户输入 1 的时候(存入内存),t1 线程就无法读取通过 t2 线程更新之后的值了,也就不会结束。在上面的程序中,t2 线程修改后的值无法被 t1 线程看到,这就是可见性导致的线程安全问题,由于编译器我们没办法更改,所以只好另寻他法。如果我们能让循环执行的慢一点,是不是就能解决问题了?尝试优化该程序:public static void main2(String[] args) { Thread t1 = new Thread(() -> { while (flag1 == 0) { // 当 flag1 为 0 时一直循环 try { Thread.sleep(1); // 放慢读取速度 } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag1 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag1 的值:"); flag1 = in.nextInt(); }); t1.start(); t2.start();}一键获取完整项目代码java在运行优化版的程序后,发现问题被解决了,其原因是:使用了 sleep(1)—— 表示每一次读取的时候休眠 1毫秒,这对于读取操作的速度来说已经很慢了。因此不会触发编译器优化,也就不会出现可见性导致的线程安全问题了。但是,在实际开发环境中频繁使用 sleep 的话会导致程序效率下降,这样的话用户体验就会变差。Java 中使用 volatile 关键字来处理 可见性 导致的线程安全问题。使用 volatile 关键字来修饰共享变量,这样一来这个变量无论如何都不会被编译器优化。具体流程是:当 t1 线程读取被修饰的变量时,会强制读取主内存中变量最新的值;当 t2 线程修改被修饰的变量时,在工作内存(寄存器)中更新变量的值之后,立即将改变的值刷新至主内存。加上 volatile 关键字强制读取内存,虽然速度慢了,但是保证数据不出错。使用 volatile 关键字优化后的程序:// 使用 volatile 关键字来修饰被读取的变量,此时无论读取速度怎样,JVM 都不会对该变量进行优化private volatile static int flag = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { while (flag == 0) { // 当 flag 为 0 时一直循环 } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag 的值:"); flag = in.nextInt(); }); t1.start(); t2.start();}一键获取完整项目代码java注意:volatile 关键字只能保证可见性,不能保证原子性,因此不能完全解决线程安全问题。当多线程中存在 ++、--、+=、-=、*= 和 /= 或者类似的操作符时就不能够使用 volatile 关键字来处理,需要使用 synchronized 关键字。五、死锁那么话又说回来,如果对锁使用不当,就很容易出现死锁。如果一个线程已经获取了锁,再次获取这个锁,能成功吗?会出现死锁吗?Object locker = new Object(); synchronized (locker) { synchronized (locker) { // 一些需要保护的逻辑 }}一键获取完整项目代码java按照前面的逻辑,第一次获取锁对象后,该锁对象已被占有,下一次获取该锁对象时应该会触发阻塞等待。而要想解除阻塞等待就得往下继续执行,但是要想往下执行就得将锁解开。这样不就构成死锁了嘛?不妨试试在你的 IDE 上运行一下~5.1 synchronized 的可重入性如果你在 IDE 上运行了刚刚的程序,可以发现程序是没问题的。按照我们这之前的逻辑,当锁对象第一次被获取到,其他的线程再想获取锁对象时就只能阻塞等待。但是,第二次获取锁对象的是同一个线程呀~ 因此并不会触发阻塞等待。Java 中的 synchronized 关键字是具有可重入性的,即对于同一个线程可以重复获取同一个锁对象。可重入锁内部包含了“线程持有者”和“计数器”两个信息,若线程加锁时发现锁已被占有且恰好是自己,此时仍可以获取到锁,让计数器自增即可;当计数器为 0 时,将锁释放,其他线程可以才获取到锁。这样就可以避免死锁的出现——开发 JDK 的前人们真的为我们考虑了太多😭5.2 死锁的概念当有两个线程为了保护两个不同的共享资源而使用两个不同的锁且这两个锁使用不当时,就会造成两个线程都在等待对方解锁,在没有外界干扰的情况下他们会一直相互等待,此时就是发生了死锁。死锁需要同时具备以下四个条件:互斥条件:要求同一个资源不能被多个线程同时占有。不可剥夺条件:当资源已被某个线程占有,其他线程只能等到该线程使用完并释放后才能获取,不可以强行打断并获取。持有并等待条件:有三个线程两个资源,当线程 1 已经占有资源 A(线程 2 尝试获取资源 A 但触发阻塞等待)且尝试获取资源 B 时(此时资源 B 已被线程 3 占有),线程 1 就会触发阻塞等待且不释放手中持有的资源 A 。循环等待/环路等待条件:有两个线程两个资源,线程 1 已占有资源 A 并想要获取资源 B ,但是资源 B 已被线程 2 占有,并且线程 2 在占有资源 B 的同时想要获取资源 A 。比如,有两个线程 t1 和 t2 以及两个锁 locker1 和 locker2,t1 先获取 locker1 并尝试获取 locker2;t2 先获取 locker2 并尝试获取 locker1:// 两个线程两把锁,每个线程获取到一把锁之后尝试获取对方的锁public static void main2(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); // 确保 t2 线程拿到 locker2 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); } } }); Thread t2 = new Thread(() -> { synchronized (locker2) { // t2 线程获取到锁对象 locker2 try { Thread.sleep(1000); // 确保 t1 线程拿到 locker1 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker1 synchronized (locker1) { System.out.println("t2 线程获取到两把锁"); } } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java运行该程序会发现什么也没输出,程序却还在运行。这就是发生了死锁。 我们借助第三方工具来观察线程的状态: 5.3 如何避免死锁要想避免死锁,我们就要从死锁的四个条件入手,其中,互斥条件和不可剥夺条件基本上是无法打破的,因为这两个是 synchronized 锁的基本特性,因此我们选择从后两个条件入手。1. 持有并等待条件:通常是由于代码中的嵌套加锁导致的,因此选择将嵌套的加锁代码改成串行的加锁代码(先将已占有的资源释放掉,然后去获取另一个资源):// 避免死锁的写法:打破持有并等待条件——将嵌套加锁改成串行加锁public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // t1 线程释放已占有的锁对象 locker1 // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); // t1 获取到两把锁之后结束执行,此时 locker1 和 locker2 均被释放 } }); Thread t2 = new Thread(() -> { synchronized (locker2) { // t2 线程获取到锁对象 locker2 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // t2 线程释放已占有的锁对象 locker2 // 尝试获取锁对象 locker1 synchronized (locker1) { System.out.println("t2 线程获取到两把锁"); // t2 获取到两把锁后结束执行 } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java2. 循环等待/环路等待条件:这种情况一般都是双方都持有对方想要的锁但是都不肯释放,所以我们就调整一下顺序:让 t1 和 t2 获取锁的顺序都是 locker1 和 locker2,当 t1 已占有 locker1 时 t2 想获取只能阻塞等待,但这时 t1 可以获取 locker2,等待 t1 两把锁都获取到并释放之后,t2 被唤醒并且获取两把锁。// 防止死锁的写法:打破循环等待条件——调整加锁顺序public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); // 此时 t2 因 locker1 被 t1 获取而处于阻塞状态 BLOCKED // t1 休眠结束后,获取 locker2 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); // t1 获取到两把锁之后结束执行,此时 locker1 和 locker2 均被释放 } } }); Thread t2 = new Thread(() -> { synchronized (locker1) { // t2 线程获取到锁对象 locker1 // 因 t1 先获取到 locker1 ,t2 此时处于阻塞状态 BLOCKED // 当 t1 结束执行后,t2 恢复执行并获取 locker1 try { Thread.sleep(1000); } catch (InterruptedException e) { // 休眠结束后继续获取 locker2 throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t2 线程获取到两把锁"); // t2 获取到两把锁后结束执行 } } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java今天暂且到这吧~————————————————原文链接:https://blog.csdn.net/REGARD712/article/details/155878355
-
Bean 生命周期的详细步骤第一阶段:Bean 的元数据配置与容器启动配置元数据:首先,你需要通过 XML、Java 注解(如 @Component, @Service, @Autowired)或 Java 配置类(@Configuration, @Bean)来定义 Bean。容器启动:Spring 容器(如 ApplicationContext)启动,加载并解析这些配置元数据,生成每个 Bean 的 BeanDefinition 对象,它包含了创建一个 Bean 所需的所有信息(如类名、作用域、是否懒加载等)。第二阶段:Bean 的实例化与初始化(核心生命周期)1. 实例化(Instantiation)描述:容器首先会调用 Bean 的构造函数(或工厂方法)来创建一个新的实例。此时只是一个简单的 Java 对象,还没有进行依赖注入,我们可以把它比作“新生儿”。扩展点:无直接扩展点。2. 依赖注入(Populate Properties)描述:Spring 根据配置(如 @Autowired, @Value)将所需的依赖注入到 Bean 的对应属性中。这一步填充了 Bean 的“血肉”。扩展点:无直接扩展点。3. Aware 接口回调(Aware Interface Injection)描述:如果 Bean 实现了各种 Aware 接口,Spring 会在此阶段回调相应的方法,将一些容器相关的对象(如 BeanNameAware, BeanFactoryAware, ApplicationContextAware)注入到 Bean 中。扩展点:BeanNameAware:设置 Bean 的 ID/Name。BeanFactoryAware:设置当前的 BeanFactory。ApplicationContextAware:设置当前的 ApplicationContext(功能最全)。EnvironmentAware:设置 Environment 对象(用于获取配置文件属性)等。4. BeanPostProcessor 前置处理描述:这是极其重要的扩展点。所有实现了 BeanPostProcessor 接口的 Bean,它们的 postProcessBeforeInitialization 方法会在这个阶段被调用。它可以对 Bean 进行包装或增强,返回一个可能是代理对象的 Bean。扩展点:BeanPostProcessor.postProcessBeforeInitialization(Object bean, String beanName)5. 初始化(Initialization)描述:Bean 的“成人礼”。在这个阶段,Bean 的初始化逻辑会被执行。a. InitializingBean 接口:如果 Bean 实现了 InitializingBean 接口,会调用其 afterPropertiesSet() 方法。b. 自定义初始化方法:调用通过 @Bean(initMethod = "...") 或 XML 中 init-method 属性指定的自定义初始化方法。扩展点:InitializingBean.afterPropertiesSet()自定义 init-method6. BeanPostProcessor 后置处理描述:这是另一个极其重要的扩展点。所有 BeanPostProcessor 的 postProcessAfterInitialization 方法会被调用。Spring AOP 就是基于此实现的。如果一个 Bean 需要被代理,通常在这里返回一个代理对象来包装目标 Bean。扩展点:BeanPostProcessor.postProcessAfterInitialization(Object bean, String beanName)7. Bean 就绪(Ready)描述:经过以上所有步骤,Bean 已经完全创建、初始化并可能被代理。它被存放在 Spring 容器(单例池)中,可以被应用程序正常获取和使用了。第三阶段:Bean 的使用与销毁8. 使用期描述:在应用程序运行期间,Bean 被其他组件依赖和调用,执行业务逻辑。9. 销毁(Destruction)描述:当 Spring 容器(通常是 ApplicationContext)被关闭时,它会开始销毁容器中的所有单例 Bean。a. DisposableBean 接口:如果 Bean 实现了 DisposableBean 接口,会调用其 destroy() 方法。b. 自定义销毁方法:调用通过 @Bean(destroyMethod = "...") 或 XML 中 destroy-method 属性指定的自定义销毁方法。常用于释放资源,如关闭数据库连接、文件句柄等。扩展点:DisposableBean.destroy()自定义 destroy-method特殊情况的处理作用域(Scope):上述生命周期主要针对单例(Singleton) Bean。对于原型(Prototype) 作用域的 Bean,Spring 容器只负责到第 6 步(初始化完成),之后就将 Bean 交给客户端,不再管理其生命周期,因此不会调用销毁方法。延迟初始化(Lazy):标记为 @Lazy 的 Bean,只有在第一次被请求时才会触发上述初始化过程,而不是在容器启动时。总结与记忆技巧可以把这个过程想象成一个人的一生:实例化:出生依赖注入:接受教育,获取生存技能(依赖)Aware 接口:获得身份ID、认识家庭和社会(容器环境)BeanPostProcessor(前):步入社会前的指导初始化:举行成人礼,开始独立承担责任BeanPostProcessor(后):步入社会后的包装/历练(可能被“代理”)就绪:成为社会栋梁,贡献力量销毁:退休,安享晚年,处理身后事————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/154697287
-
什么是阻塞队列?阻塞队列是一种特殊的队列,它在数据结构的基础上附加了两个额外的操作特性:阻塞插入:当队列已满时,尝试向队列中插入元素的线程会被阻塞,直到队列中有空闲位置。阻塞移除:当队列为空时,尝试从队列中获取元素的线程会被阻塞,直到队列中有新的元素被加入。简单来说,阻塞队列是一个线程安全的、支持阻塞等待的生产者-消费者模型的核心容器。阻塞队列的实现原理阻塞队列的实现原理主要依赖于 锁(Lock) 和 条件变量(Condition)。在Java中,这通常通过 ReentrantLock 和 Condition 来实现。我们以一个简单的有界数组阻塞队列为例,剖析其核心原理:核心组件一个队列:通常用数组或链表实现,用于存储元素。一把锁:一个 ReentrantLock,用于保证所有操作的线程安全性。两个条件变量:notEmpty:一个与锁绑定的条件,用于表示“队列非空”。当消费者因队列为空而等待时,会挂在这个条件上。当生产者放入一个新元素后,会唤醒挂在这个条件上的线程。notFull:一个与锁绑定的条件,用于表示“队列未满”。当生产者因队列已满而等待时,会挂在这个条件上。当消费者取走一个元素后,会唤醒挂在这个条件上的线程。核心方法原理put(E e) 方法(阻塞插入)获取锁。while (队列已满):调用 notFull.await() 释放锁并进入等待状态。当被其他线程唤醒并重新获得锁后,再次检查队列是否已满(防止虚假唤醒)。将元素 e 入队。调用 notEmpty.signal() 或 notEmpty.signalAll(),唤醒一个或所有正在 notEmpty 上等待的消费者线程。释放锁。take() 方法(阻塞移除)获取锁。while (队列为空):调用 notEmpty.await() 释放锁并进入等待状态。当被其他线程唤醒并重新获得锁后,再次检查队列是否为空。将队首元素出队。调用 notFull.signal() 或 notFull.signalAll(),唤醒一个或所有正在 notFull 上等待的生产者线程。释放锁。关键点总结:线程安全:所有对队列结构的修改都在锁的保护下进行。高效等待/通知:使用 Condition 的 await() 和 signal() 代替传统的 Object.wait() 和 Object.notify(),可以更精确地控制等待和唤醒的线程类型(生产者或消费者),避免了“惊群效应”。循环检查条件:在从 await() 返回后,必须重新检查条件(队列是否满/空),这是应对“虚假唤醒”的标准做法。如何使用阻塞队列实现生产者-消费者模型生产者-消费者模型是一种经典的多线程协作模式,它通过一个共享的缓冲区(即阻塞队列) 来解耦生产者和消费者,使他们不必直接通信,而是各自以不同的速率对缓冲区进行操作。阻塞队列天生就是为这个模型设计的,使用它来实现非常简单优雅。实现步骤创建阻塞队列:选择一个合适的阻塞队列实现,例如 ArrayBlockingQueue。创建生产者线程:生产者线程循环生产数据,并调用 queue.put(data) 将数据放入队列。如果队列满,put 方法会自动阻塞,直到有空间。创建消费者线程:消费者线程循环调用 queue.take() 从队列中获取数据。如果队列空,take 方法会自动阻塞,直到有数据可用。启动线程:启动生产者和消费者线程,它们会自动协作。代码示例import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue; public class ProducerConsumerExample { public static void main(String[] args) { // 1. 创建一个容量为10的阻塞队列 BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // 2. 创建生产者线程 Thread producerThread = new Thread(() -> { try { int value = 0; while (true) { // 生产数据 queue.put(value); System.out.println("Produced: " + value); value++; // 模拟生产耗时 Thread.sleep(1000); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // 3. 创建消费者线程 Thread consumerThread = new Thread(() -> { try { while (true) { // 消费数据 Integer value = queue.take(); System.out.println("Consumed: " + value); // 模拟消费耗时 Thread.sleep(2000); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // 4. 启动线程 producerThread.start(); consumerThread.start(); }}一键获取完整项目代码代码分析生产者:每秒生产一个数字(0, 1, 2...),并放入队列。如果队列已满(本例中容量为10),生产者会在 put 方法处阻塞,等待消费者消费。消费者:每两秒从队列中取出一个数字。如果队列为空,消费者会在 take 方法处阻塞,等待生产者生产。运行结果:你会看到生产者生产的速度快于消费者,但由于队列的存在,生产者不会丢失数据。当队列满后,生产者会停下来等待。整个系统平稳运行,生产者和消费者速率不匹配的问题被阻塞队列完美解决。Java中的阻塞队列实现Java的 java.util.concurrent 包提供了多种现成的阻塞队列实现,可以直接使用:ArrayBlockingQueue:基于数组的有界阻塞队列。LinkedBlockingQueue:基于链表的阻塞队列,可选有界或无界。PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。它实现了数据的直接传递。DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。总结阻塞队列是一个线程安全的、支持阻塞插入和移除的队列,是生产者-消费者模型的理想载体。实现原理核心是锁+条件变量,通过精确的等待/通知机制来协调生产者和消费者的步调。使用方式极其简单,生产者调用 put,消费者调用 take,无需开发者手动处理线程同步和通信问题,大大简化了并发编程的难度。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/153782844
-
一、核心原理1. 数据存储结构// 每个 Thread 对象内部都有一个 ThreadLocalMapThreadLocal.ThreadLocalMap threadLocals = null; // ThreadLocalMap 内部使用 Entry 数组,Entry 继承自 WeakReference<ThreadLocal<?>>static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); // 弱引用指向 ThreadLocal 实例 value = v; // 强引用指向实际存储的值 }}一键获取完整项目代码2. 关键设计线程隔离:每个线程有自己的 ThreadLocalMap 副本哈希表结构:使用开放地址法解决哈希冲突弱引用键:Entry 的 key(ThreadLocal 实例)是弱引用延迟清理:set / get 时自动清理过期条目二、源码分析1. set() 方法流程public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); // this指当前ThreadLocal实例 } else { createMap(t, value); }} private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // 遍历查找合适的位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 找到相同的key,直接替换value if (k == key) { e.value = value; return; } // key已被回收,替换过期条目 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; // 清理并判断是否需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}一键获取完整项目代码2. get() 方法流程public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); // 返回初始值}一键获取完整项目代码三、使用场景1. 典型应用场景// 场景1:线程上下文信息传递(如Spring的RequestContextHolder)public class RequestContextHolder { private static final ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>(); public static void setRequest(HttpServletRequest request) { requestHolder.set(request); } public static HttpServletRequest getRequest() { return requestHolder.get(); }} // 场景2:数据库连接管理public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> DriverManager.getConnection(url)); public static Connection getConnection() { return connectionHolder.get(); }} // 场景3:用户会话信息public class UserContext { private static ThreadLocal<UserInfo> userHolder = new ThreadLocal<>(); public static void setUser(UserInfo user) { userHolder.set(user); } public static UserInfo getUser() { return userHolder.get(); }} // 场景4:避免参数传递public class TransactionContext { private static ThreadLocal<Transaction> transactionHolder = new ThreadLocal<>(); public static void beginTransaction() { transactionHolder.set(new Transaction()); } public static Transaction getTransaction() { return transactionHolder.get(); }}一键获取完整项目代码2. 使用建议声明为 private static final考虑使用 ThreadLocal.withInitial() 提供初始值在 finally 块中清理资源四、内存泄漏问题1. 泄漏原理强引用链:Thread → ThreadLocalMap → Entry[] → Entry → value (强引用) 弱引用: Entry → key (弱引用指向ThreadLocal) 泄漏场景:1. ThreadLocal实例被回收 → key=null2. 但value仍然被Entry强引用3. 线程池中线程长期存活 → value无法被回收4. 导致内存泄漏一键获取完整项目代码2. 解决方案对比// 方案1:手动remove(推荐)try { threadLocal.set(value); // ... 业务逻辑} finally { threadLocal.remove(); // 必须执行!} // 方案2:使用InheritableThreadLocal(父子线程传递)ThreadLocal<String> parent = new InheritableThreadLocal<>();parent.set("parent value"); new Thread(() -> { // 子线程可以获取父线程的值 System.out.println(parent.get()); // "parent value"}).start(); // 方案3:使用FastThreadLocal(Netty优化版)// 适用于高并发场景,避免了哈希冲突一键获取完整项目代码3. 最佳实践public class SafeThreadLocalExample { // 1. 使用static final修饰 private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); // 2. 包装为工具类 public static Date parse(String dateStr) throws ParseException { SimpleDateFormat sdf = DATE_FORMAT.get(); try { return sdf.parse(dateStr); } finally { // 注意:这里通常不需要remove,因为要重用SimpleDateFormat // 但如果是用完即弃的场景,应该remove } } // 3. 线程池场景必须清理 public void executeInThreadPool() { ExecutorService executor = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { executor.submit(() -> { try { UserContext.setUser(new UserInfo()); // ... 业务处理 } finally { UserContext.remove(); // 关键! } }); } }}一键获取完整项目代码五、注意事项线程池风险:线程复用导致数据污染继承问题:子线程默认无法访问父线程的ThreadLocal性能影响:哈希冲突时使用线性探测,可能影响性能空值处理:get()返回null时要考虑初始化六、替代方案方案适用场景优点缺点ThreadLocal线程隔离数据简单高效内存泄漏风险InheritableThreadLocal父子线程传递继承上下文线程池中失效TransmittableThreadLocal线程池传递线程池友好引入依赖参数传递简单场景无副作用代码冗余七、调试技巧// 查看ThreadLocalMap内容(调试用)public static void dumpThreadLocalMap(Thread thread) throws Exception { Field field = Thread.class.getDeclaredField("threadLocals"); field.setAccessible(true); Object map = field.get(thread); if (map != null) { Field tableField = map.getClass().getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(map); for (Object entry : table) { if (entry != null) { Field valueField = entry.getClass().getDeclaredField("value"); valueField.setAccessible(true); System.out.println("Key: " + ((WeakReference<?>) entry).get() + ", Value: " + valueField.get(entry)); } } }}一键获取完整项目代码ThreadLocal 是强大的线程隔离工具,但需要谨慎使用。在 Web 应用和线程池场景中,必须在 finally 块中调用 remove(),这是避免内存泄漏的关键。面试回答关于 ThreadLocal,我从原理、场景和内存泄漏三个方面来说一下我的理解。1. 首先,它的核心原理是什么?简单来说,ThreadLocal 是一个线程级别的变量隔离工具。它的设计目标就是让同一个变量,在不同的线程里有自己独立的副本,互不干扰。底层结构:每个线程(Thread对象)内部都有一个自己的 ThreadLocalMap(你可以把它想象成一个线程私有的、简易版的HashMap)。怎么存:当我们调用 ThreadLocal.set(value) 时,实际上是以当前的 ThreadLocal 实例自身作为 Key,要保存的值作为 Value,存入当前线程的那个 ThreadLocalMap 里。怎么取:调用 ThreadLocal.get() 时,也是用自己作为 Key,去当前线程的 Map 里查找对应的 Value。打个比方:就像去银行租保险箱。Thread 是银行,ThreadLocalMap 是银行里的一排保险箱,ThreadLocal 实例就是你手里那把特定的钥匙。你用这把钥匙(ThreadLocal实例)只能打开属于你的那个格子(当前线程的Map),存取自己的东西(Value),完全看不到别人格子的东西。不同的人(线程)即使用同一款钥匙(同一个ThreadLocal实例),打开的也是不同银行的格子,东西自然隔离了。2. 其次,它的典型使用场景有哪些?正是因为这种线程隔离的特性,它特别适合用来传递一些需要在线程整个生命周期内、多个方法间共享,但又不能(或不想)通过方法参数显式传递的数据。最常见的有两个场景:场景一:保存上下文信息(最经典)比如在 Web 应用 或 RPC 框架 中处理一个用户请求时,这个请求从进入系统到返回响应,全程可能由同一个线程处理。我们会把一些信息(比如用户ID、交易ID、语言环境)存到一个 ThreadLocal 里。这样,后续的任何业务方法、工具类,只要在同一个线程里,就能直接 get() 到这些信息,避免了在每一个方法签名上都加上这些参数,代码会简洁很多。场景二:管理线程安全的独享资源典型例子是 数据库连接 和 SimpleDateFormat。像 SimpleDateFormat 这个类,它不是线程安全的。如果做成全局共享,就要加锁,性能差。用 ThreadLocal 的话,每个线程都拥有自己的一个 SimpleDateFormat 实例,既避免了线程安全问题,又因为线程复用了这个实例,减少了创建对象的开销。类似的,在一些需要保证数据库连接线程隔离(比如事务管理)的场景,也会用到 ThreadLocal 来存放当前线程的连接。3. 最后,关于它的内存泄漏问题ThreadLocal 如果使用不当,确实可能导致内存泄漏。它的根源在于 ThreadLocalMap 中 Entry 的设计。问题根源:ThreadLocalMap 的 Key(也就是 ThreadLocal 实例)是一个 弱引用。这意味着,如果外界没有强引用指向这个 ThreadLocal 对象(比如我们把 ThreadLocal 变量设为了 null),下次垃圾回收时,这个 Key 就会被回收掉,于是 Map 里就出现了一个 Key 为 null,但 Value 依然存在的 Entry。这个 Value 是一个强引用,只要线程还活着(比如用的是线程池,线程会复用,一直不结束),这个 Value 对象就永远无法被回收,造成了内存泄漏。如何避免:良好习惯:每次使用完 ThreadLocal 后,一定要手动调用 remove() 方法。这不仅是清理当前值,更重要的是它会清理掉整个 Entry,这是最有效、最安全的做法。设计保障:ThreadLocal 本身也做了一些努力,比如在 set()、get()、remove() 的时候,会尝试去清理那些 Key 为 null 的过期 Entry。但这是一种“被动清理”,不能完全依赖。代码层面:尽量将 ThreadLocal 变量声明为 static final,这样它的生命周期就和类一样长,不会被轻易回收,减少了产生 null Key 的机会。但这并不能替代 remove(),因为线程池复用时,上一个任务的值可能会污染下一个任务。总结一下:内存泄漏的关键是 “弱Key + 强Value + 长生命周期线程” 的组合。所以,把 remove() 放在 finally 块里调用,是一个必须养成的编程习惯。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/156130616
-
概述Redis 主从复制是一种数据同步机制,它允许一个 Redis 服务器(称为 主服务器/Master)将其数据复制到一个或多个 Redis 服务器(称为 从服务器/Slave/Replica)。这是 Redis 实现高可用性、可扩展性和数据冗余的核心技术之一。一、核心作用数据冗余与备份:核心作用:从服务器是主服务器数据的实时热备份。当主服务器数据丢失或损坏时,可以从从服务器恢复,是实现数据持久化的另一种有效方式。读写分离与负载均衡:核心作用:主服务器通常处理写操作和强一致性读操作,而从服务器可以处理大量的读操作。通过将读请求分流到多个从服务器上,可以显著提升系统的整体读吞吐量和并发能力。注意:由于复制是异步的,从服务器上的数据可能存在毫秒级的延迟,适用于对数据一致性要求不非常严格的读场景(如缓存、报表查询)。高可用和故障恢复的基石:核心作用:主从复制是构建 Redis Sentinel(哨兵) 和 Redis Cluster(集群) 等高可用架构的基础。当主服务器发生故障时,可以通过哨兵自动将一个从服务器提升为新的主服务器,实现服务快速切换,保证业务的连续性。横向扩展读能力:核心作用:当读请求成为瓶颈时,可以简单地通过添加更多从服务器来线性扩展读性能,而无需升级主服务器硬件。二、详细工作原理整个复制过程可以分为三个阶段:连接建立阶段、数据同步阶段、命令传播阶段。阶段 1:连接建立与配置配置从节点:在从服务器配置文件 (redis.conf) 中设置 replicaof <master-ip> <master-port> 或在运行时使用 REPLICAOF 命令。建立连接:从服务器根据配置,向主服务器发起一个 Socket 连接,并发送 PING 命令检查通信是否正常。身份验证:如果主服务器设置了 requirepass,从服务器需要发送 AUTH 命令进行密码验证。端口监听:从服务器还会建立一个 复制积压缓冲区监听端口,等待主服务器后续发送数据。阶段 2:数据同步(全量/部分同步)这是复制过程中最核心、最复杂的部分。Redis 2.8 之后,使用 PSYNC 命令取代了旧的 SYNC 命令,支持部分重同步,极大地优化了断线重连后的效率。 全量同步:触发条件:从服务器是第一次连接主服务器,或者从服务器记录的复制偏移量已经不在主服务器的复制积压缓冲区中。过程:从服务器发送 PSYNC ? -1 命令请求全量同步。主服务器执行 BGSAVE 命令,在后台生成当前数据的 RDB 快照文件。主服务器将 RDB 文件通过网络发送给从服务器。在生成和传输RDB期间,新的写命令会被缓存在内存的复制客户端缓冲区中。从服务器清空自身旧数据,然后加载接收到的 RDB 文件,将自身数据状态更新到与主服务器执行 BGSAVE 时一致。主服务器将复制客户端缓冲区中积累的写命令发送给从服务器,从服务器执行这些命令,最终达到与主服务器完全一致的状态。缺点:非常消耗主服务器的 CPU、内存、磁盘 I/O 和网络带宽,尤其是数据量大时。 部分同步:触发条件:从服务器短时间断开后重连,并且它之前同步的偏移量仍然存在于主服务器的复制积压缓冲区中。过程:从服务器发送 PSYNC <runid> <offset> 命令,其中 runid 是主服务器的唯一ID,offset 是从服务器当前的复制偏移量。主服务器判断 runid 是否与自己一致,且 offset 之后的数据是否还在复制积压缓冲区内。如果条件满足,主服务器回复 +CONTINUE,然后仅将从 offset 到缓冲区末尾的写命令发送给从服务器。优点:效率极高,只传输少量缺失的数据,对资源影响小。关键概念解释:复制偏移量:主从服务器各自维护一个偏移量计数器。主服务器每次传播N个字节的命令,其偏移量就增加N。从服务器每次接收到N个字节,其偏移量也增加N。通过对比偏移量可以判断数据是否一致。复制积压缓冲区:主服务器维护的一个固定长度的先进先出队列。它持续记录最近传播的写命令。其大小通过 repl-backlog-size 配置,是决定能否进行部分同步的关键。服务器运行ID:每个Redis实例启动时都会生成一个唯一的运行ID。从服务器会记录主服务器的ID。当主服务器重启变更后,运行ID改变,从服务器会触发全量同步。阶段 3:命令传播(增量同步)数据同步完成后,复制进入稳定阶段。持续同步:主服务器每执行一个会修改数据集的写命令(如 SET、LPUSH 等),都会异步地将这个命令发送给所有连接的从服务器。从服务器执行:从服务器接收到命令后,会在自身的数据集上执行相同的命令,从而保持与主服务器的最终一致性。异步特性:整个命令传播过程是异步的。主服务器发送命令后不会等待从服务器的回复。这意味着在极端情况下,如果主服务器在命令发送后立即宕机,该命令可能丢失,导致从服务器数据稍旧。这是Redis复制在性能和强一致性之间做的权衡。三、重要特性与配置异步复制:默认且最常用的模式,性能好,但存在数据丢失的极小窗口期。可配置的“最小副本数”:通过 min-replicas-to-write 和 min-replicas-max-lag 配置,可以让主服务器在连接的从服务器数量不足或延迟过高时,拒绝执行写命令。这在一定程度上提高了数据的安全性,牺牲了部分可用性。无磁盘复制:通过 repl-diskless-sync 配置,主服务器可以直接将 RDB 内容通过网络发送给从服务器,而不需要先落盘。适用于磁盘IO慢的网络环境。级联复制:从服务器也可以有自己的从服务器,形成树状复制结构,可以减轻主服务器在传播命令时的网络压力。四、总结与形象比喻你可以将 Redis 主从复制理解为一个 “出版-订阅”模型 或 “领导-跟随”模型:主服务器 就像出版社,负责撰写和出版新书(写命令)。复制积压缓冲区 就像是出版社的近期稿件仓库。从服务器 就像各地的书店。全量同步 就像书店第一次进货,需要把出版社的所有库存书籍(RDB)全部运过来。部分同步 就像书店临时补货,只从出版社的近期稿件仓库里拿最新出版的那几本书。命令传播 就像出版社每出版一本新书,就立即寄送给所有订阅的书店。作用:这个系统让书店(从服务器)始终有书可卖(数据可读),即使总社(主服务器)暂时关闭,也能从其他大型书店(另一个从服务器)调货,保证了图书销售系统(Redis服务)的稳定和高效。面试回答Redis 主从复制主要用来实现数据的冗余备份、读写分离和高可用。它的核心就是让一个主节点的数据自动同步到一个或多个从节点上。 原理上,我把它分为三个阶段:建立连接阶段从节点启动后,会通过 slaveof 命令或者配置指向主节点,然后向主节点发送 PSYNC 命令请求同步。主节点收到请求后,会生成一个 RDB 快照文件(bgsave 方式),同时用缓冲区记录这期间的新写命令。数据同步阶段主节点把生成的 RDB 文件发送给从节点,从节点清空自己的旧数据,然后加载这个 RDB 来恢复数据。如果在生成 RDB 期间主节点有新的写操作,这些命令会先保存在一个叫“复制缓冲区”的地方。命令传播阶段RDB 同步完成之后,主节点会把复制缓冲区里的写命令以及后续的所有写命令,以 AOF 重放的方式发送给从节点,从节点执行这些命令,从而和主节点保持实时一致。之后主节点每执行一个写命令,都会异步发送给从节点。另外,Redis 2.8 之后支持了部分重同步:如果从节点短暂断连后又恢复,主节点可以根据复制偏移量和复制缓冲区,只发送断开期间缺失的那部分命令,而不需要全量同步,这大大提升了复制的效率。主从复制的作用主要有三点:数据备份从节点相当于主节点的一个实时备份,一旦主节点数据丢失,可以从从节点恢复。读写分离主节点负责写,从节点可以分担读请求,这样提升整体读的吞吐量,适合读多写少的场景。高可用基础主从复制是 Redis Sentinel 和 Redis Cluster 实现高可用的基础,主节点挂了之后,可以手动或自动将一个从节点提升为主节点,继续提供服务。如果小假的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!————————————————版权声明:本文为CSDN博主「程序员小假」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/2402_87298751/article/details/155789785
-
1.概述Spring事务管理是Spring框架中用于确保数据库操作 原子性、一致性、隔离性和持久性(ACID)的核心机制。它通过声明式或编程式(本文略)方式管理事务,支持多种事务传播行为和隔离级别相较于编程式事务,声明式事务通过@Transactional注解实现事务管理,无需手动编写事务代码事务基本概念在全面解析MySQL(5)——“索引、事务、JDBC”三大核心一文中有介绍,本文不再赘述2.@Transactional作用:提供声明式事务管理。它简化了在应用程序中管理数据库事务的流程。开发者只需在方法或类上添加此注解,Spring框架就会自动处理事务的开启、提交和回滚,无需手动编写事务管理代码(如 begin、commit、rollback)级别:类 + 方法作为类注解:为类中所有public方法添加注解作为方法注解:默认仅对public方法生效@RequestMapping("/test")@RestController@Slf4jpublic class TestController { private final UserService userService; @Autowired public TestController(UserService userService) { this.userService = userService; } @Transactional @RequestMapping("/test1") public String test1(String userName,String password) { UserInfo userInfo = new UserInfo(); userInfo.setUserName(userName); userInfo.setPassword(password); Integer result = userService.register(userInfo); if (result == 1){ log.info("test1注册成功,userName:{},password:{}", userName, password); } return "注册成功"; }}使用PostMan向后端发送请求:MySQL查询结果如下:后端日志日志如下:2.1 rollbackfor作用:指定哪些异常触发回滚,默认情况下在抛出 非受查异常(RuntimeException)/错误(Error) 时触发回滚抛出受查异常时@RequestMapping("/test")@RestController@Slf4jpublic class TestController { private final UserService userService; @Autowired public TestController(UserService userService) { this.userService = userService; } @Transactional @RequestMapping("/test2") public String test2(String userName,String password) throws IOException { UserInfo userInfo = new UserInfo(); userInfo.setUserName(userName); userInfo.setPassword(password); Integer result = userService.register(userInfo); if (result == 1){ log.info("test2注册成功,userName:{},password:{}", userName, password); throw new IOException(); } return "注册成功"; }}使用PostMan向后端发送请求:MySQL查询结果如下:后端日志日志如下:抛出非受查异常时@RequestMapping("/test")@RestController@Slf4jpublic class TestController { private final UserService userService; @Autowired public TestController(UserService userService) { this.userService = userService; } @Transactional @RequestMapping("/test3") public String test3(String userName,String password) { UserInfo userInfo = new UserInfo(); userInfo.setUserName(userName); userInfo.setPassword(password); Integer result = userService.register(userInfo); if (result == 1){ log.info("test3注册成功,userName:{},password:{}", userName, password); throw new RuntimeException(); } return "注册成功"; }}使用PostMan向后端发送请求:MySQL查询结果如下:后端日志日志如下:指定回滚类型@RequestMapping("/test")@RestController@Slf4jpublic class TestController { private final UserService userService; @Autowired public TestController(UserService userService) { this.userService = userService; } @Transactional(rollbackFor = Exception.class) @RequestMapping("/test4") public String test4(String userName,String password) throws IOException { UserInfo userInfo = new UserInfo(); userInfo.setUserName(userName); userInfo.setPassword(password); Integer result = userService.register(userInfo); if (result == 1){ log.info("test4注册成功,userName:{},password:{}", userName, password); throw new IOException(); } return "注册成功"; }}使用PostMan向后端发送请求:MySQL查询结果如下:后端日志日志如下:2.2 isolation作用:用于指定事务的隔离级别Isolation.DEFAULT:使用底层数据库默认的隔离级别Isolation.READ_UNCOMMITTED:读未提交Isolation.READ_COMMITTED:读已提交Isolation.REPEATABLE_READ:可重复读Isolation.SERIALIZABLE:串行化每种隔离级别的具体效果在全面解析MySQL(5)——“索引、事务、JDBC”三大核心一文中有介绍,本文不再赘述2.3 propagation作用:用于定义事务的传播行为,即当前事务方法被另一个事务方法调用时,事务应如何传播。Spring提供了7种传播行为,均基于Propagation枚举类实现2.3.1 Propagation.REQUIRED默认传播行为。如果当前存在事务,则加入该事务;如果不存在事务,则新建一个事务 2.3.2 Propagation.SUPPORTS如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行2.3.3 Propagation.MANDATORY强制要求当前存在事务并加入,否则抛出异常 2.3.4 Propagation.REQUIRES_NEW无论当前是否存在事务,都新建一个事务。新事务与当前事务独立,互不干扰2.3.5 Propagation.NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,则挂起该事务2.3.6 Propagation.NEVER强制要求当前不能存在事务,否则抛出异常2.3.7 Propagation.NESTED如果当前存在事务,则在嵌套事务中执行;如果不存在事务,则行为与Propagation.REQUIRED相同。嵌套事务的回滚不影响外部事务,但外部事务回滚会导致嵌套事务回滚(适用于需要部分回滚的场景)3.GiteeGitee地址:九转苍翎本文源码:spring-trans————————————————版权声明:本文为CSDN博主「九转苍翎」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/2401_89167985/article/details/155503362
-
1、git删除分支实现步骤【转载】cid:link_62、git branch如何delete方式【转载】cid:link_73、 input的accept属性让文件上传安全高效【转载】cid:link_04、HTML5的<input>标签的`type`属性值详解和代码示例【转载】cid:link_15、 python爬虫脚本HTTP 403 Forbidden错误怎么办?【转载】cid:link_26、Python实现将.py代码转换为带语法高亮的Word和PDF【转载】cid:link_87、Python多进程中避免死锁问题的六种策略【转载】cid:link_98、 Python实现将PDF转DOCX的超简单教程(附源码)【转载】cid:link_39、Python基于OpenPyxl实现Excel转PDF并精准控制分页【转载】cid:link_1010、Python获取Docker容器实时资源占用(CPU、内存、IO等)5种实现方式【转载】cid:link_1111、 Python flash URL访问参数配置【转载】cid:link_1212、 Python利用PyMobileDevice3控制iOS设备的完整教程【转载】cid:link_413、 Python基本语法总结大全(含java、js对比)【转载】cid:link_514、 Python自动化提取多个Word文档的文本【转载】cid:link_1315、mybatis-plus分表实现案例(附示例代码)【转载】https://bbs.huaweicloud.com/forum/thread-02126200655519113076-1-1.html
-
一、坑 1:Token 过期未处理,鉴权异常引发服务中断问题本质DeepSeek 的 Token 一般能用 30 天,好多人一开始就把 Token 写在代码里,根本没考虑过期的事。一旦 Token 过期,所有 API 调用全返回 401 鉴权失败,如果没做异常处理,就会直接导致依赖该接口的业务服务中断。更糟的是,有的代码连异常都不捕获,直接抛个运行时异常,把线程池堵死,最后服务都熔断了。典型错误代码12345678910111213141516171819202122232425// 错误示例:硬编码Token,无过期处理、无异常捕获public class DeepSeekClient { // 写死的 Token,过期直接失效 private static final String DEEPSEEK_TOKEN = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; private static final String API_URL = "https://api.deepseek.com/v1/chat/completions"; public String callApi(String prompt) { // 直接拼 Token,不管过没过期 HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + DEEPSEEK_TOKEN); headers.setContentType(MediaType.APPLICATION_JSON); // 拼请求体 Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", prompt) )); RestTemplate restTemplate = new RestTemplate(); // 鉴权失败直接抛异常,服务跟着崩 ResponseEntity<String> response = restTemplate.postForEntity(API_URL, new HttpEntity<>(requestBody, headers), String.class); return response.getBody(); }}解决方案:实现 Token 自动刷新 + 异常兜底解决方案:实现 Token 自动刷新 + 异常兜底 核心思路:封装 Token 管理类,维护 Token 有效期,提前 1 天主动刷新;增加鉴权异常捕获,触发被动刷新;采用双重检查锁保证 Token 刷新的线程安全;增加降级策略,Token 刷新失败时触发告警并返回兜底响应。完整正确代码读取配置的工具类123456789101112131415161718192021222324import java.io.IOException;import java.io.InputStream;import java.util.Properties; // 读取配置文件的工具public class PropertiesUtils { private static final Properties props = new Properties(); static { try (InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("deepseek.properties")) { props.load(in); } catch (IOException e) { throw new RuntimeException("加载deepseek配置文件失败", e); } } public static String getProperty(String key) { String value = props.getProperty(key); if (value == null) { throw new RuntimeException("配置项[" + key + "]不存在"); } return value; }}然后是 Token 管理类和 API 调用客户端:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130import org.springframework.http.*;import org.springframework.web.client.HttpClientErrorException;import org.springframework.web.client.RestTemplate; import java.util.HashMap;import java.util.Map;import java.util.concurrent.locks.ReentrantLock; // Token 管理工具,自动刷新,线程安全public class DeepSeekTokenManager { // 从配置文件读 private static final String REFRESH_TOKEN = PropertiesUtils.getProperty("deepseek.refresh.token"); private static final String TOKEN_REFRESH_URL = "https://api.deepseek.com/v1/auth/refresh"; // 30天有效期,提前1天刷新,单位都是毫秒 private static final long TOKEN_VALID_PERIOD = 30 * 24 * 60 * 60 * 1000L; private static final long REFRESH_ADVANCE = 24 * 60 * 60 * 1000L; // 当前能用的Token、过期时间、刷新用的锁 private volatile String currentToken; private volatile long expireTime; private final ReentrantLock refreshLock = new ReentrantLock(); private final RestTemplate restTemplate = new RestTemplate(); // 单例模式 private static class SingletonHolder { private static final DeepSeekTokenManager INSTANCE = new DeepSeekTokenManager(); } public static DeepSeekTokenManager getInstance() { return SingletonHolder.INSTANCE; } // 初始化的时候就加载第一个Token private DeepSeekTokenManager() { refreshToken(); } // 拿能用的Token,自动判断要不要刷新 public String getValidToken() { long now = System.currentTimeMillis(); // 现在的时间加提前量,快到过期时间就刷新 if (now + REFRESH_ADVANCE >= expireTime) { refreshToken(); } return currentToken; } // 刷新Token private void refreshToken() { if (System.currentTimeMillis() + REFRESH_ADVANCE < expireTime) { return; } // 加锁后再检查一遍,防止刚释放锁别人又进来了 refreshLock.lock(); try { if (System.currentTimeMillis() + REFRESH_ADVANCE < expireTime) { return; } // 拼刷新Token的请求 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); Map<String, String> requestBody = new HashMap<>(); requestBody.put("refresh_token", REFRESH_TOKEN); ResponseEntity<Map> response = restTemplate.postForEntity( TOKEN_REFRESH_URL, new HttpEntity<>(requestBody, headers), Map.class ); if (response.getStatusCode() == HttpStatus.OK) { Map<String, Object> resBody = response.getBody(); this.currentToken = (String) resBody.get("access_token"); this.expireTime = System.currentTimeMillis() + TOKEN_VALID_PERIOD; System.out.println("Token刷新成功,新的过期时间:" + expireTime); } else { throw new RuntimeException("Token刷新失败,响应码:" + response.getStatusCode()); } } catch (Exception e) { System.err.println("Token刷新出问题了:" + e.getMessage()); // 临时延长10分钟,给运维留时间处理 this.expireTime = System.currentTimeMillis() + 10 * 60 * 1000L; throw new RuntimeException("Token刷新失败,已临时续命10分钟,赶紧查!", e); } finally { refreshLock.unlock(); } }} // DeepSeek API 调用客户端(集成Token管理)public class DeepSeekApiClient { private static final String API_URL = "https://api.deepseek.com/v1/chat/completions"; private final RestTemplate restTemplate = new RestTemplate(); private final DeepSeekTokenManager tokenManager = DeepSeekTokenManager.getInstance(); public String callDeepSeek(String prompt) { // 获取有效Token String token = tokenManager.getValidToken(); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); headers.setContentType(MediaType.APPLICATION_JSON); // 构建请求体 Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", prompt) )); requestBody.put("timeout", 30000); // 30秒超时 HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); try { ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class); return response.getBody(); } catch (HttpClientErrorException.Unauthorized e) { System.err.println("鉴权失败,强制刷新Token重试:" + e.getMessage()); tokenManager.refreshToken(); // 重试请求 headers.set("Authorization", "Bearer " + tokenManager.getValidToken()); HttpEntity<Map<String, Object>> retryReq = new HttpEntity<>(requestBody, headers); return restTemplate.postForEntity(API_URL, retryReq, String.class).getBody(); } catch (Exception e) { System.err.println("API调用出错:" + e.getMessage()); return "{\"choices\":[{\"message\":{\"content\":\"稍等一下,请求有点问题~\"}}]}"; } }}关键优化点说明配置化:用 properties 文件存 Token,改的时候不用动代码;主动+被动刷新:提前1天主动刷,漏了还有401被动刷,基本不会过期;线程安全:双重检查锁 + 可重入锁,多线程的时候只会有一个去刷新;降级兜底:刷新失败临时续命10分钟,API调用错了返回友好提示,服务不会直接崩。二、坑 2:并发调用线程不安全,数据错乱/连接泄漏问题本质好多人写代码的时候,RestTemplate 不配置连接池,还把 HttpHeaders 这种东西做成全局共享的。多线程一并发就出问题:多线程并发调用时,请求头/请求体数据错乱;RestTemplate 未配置连接池,高并发下出现连接泄漏、端口耗尽;未做线程池隔离,API 调用超时导致线程池阻塞,影响其他业务。典型错误代码123456789101112131415161718192021222324252627// 错误示例:共享非线程安全对象,无连接池,无线程池隔离public class UnsafeDeepSeekClient { // 错误:RestTemplate未配置连接池,高并发下连接泄漏 private static final RestTemplate restTemplate = new RestTemplate(); // 错误:共享HttpHeaders,多线程下数据错乱 private static final HttpHeaders sharedHeaders = new HttpHeaders(); static { sharedHeaders.set("Authorization", "Bearer sk-xxxxxxxx"); sharedHeaders.setContentType(MediaType.APPLICATION_JSON); } // 错误:无线程池,直接同步调用,超时阻塞主线程 public String concurrentCall(String prompt) { Map<String, Object> requestBody = new HashMap<>(); // HashMap也不是线程安全的 requestBody.put("model", "deepseek-chat"); requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", prompt) )); // 多线程下sharedHeaders可能被篡改 HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, sharedHeaders); // 无超时配置,调用超时阻塞线程 ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class); return response.getBody(); }}解决方案:线程池隔离 + 连接池配置 + 线程安全封装核心思路:配置 RestTemplate 连接池,限制最大连接数、超时时间,避免连接泄漏;使用线程池隔离 DeepSeek API 调用,避免影响核心业务;每个请求独立创建 HttpHeaders、HashMap,避免多线程共享;增加请求超时、线程池拒绝策略,保证服务稳定性。完整正确代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105import org.apache.http.client.config.RequestConfig;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClientBuilder;import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;import org.springframework.web.client.RestTemplate; import java.util.HashMap;import java.util.Map;import java.util.concurrent.*; // 线程安全的DeepSeek API客户端(含连接池、线程池配置)public class ThreadSafeDeepSeekClient { // 带连接池的RestTemplate private static final RestTemplate restTemplate; // 配置线程池:隔离DeepSeek API调用,避免影响核心业务 private static final ExecutorService deepSeekExecutor; static { // 1. 先配HTTP连接池,控制最大连接数 PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setMaxTotal(100); // 总共最多100个连接 connManager.setDefaultMaxPerRoute(50); // 同一个地址最多50个连接 // 超时配置,别卡太久 RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) // 连服务器5秒超时 .setConnectionRequestTimeout(3000) // 从连接池拿连接3秒超时 .setSocketTimeout(30000) // 读数据30秒超时 .build(); CloseableHttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connManager) .setDefaultRequestConfig(requestConfig) .build(); // 把连接池配置给RestTemplate HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); restTemplate = new RestTemplate(requestFactory); // 2. 再配个专用线程池 deepSeekExecutor = new ThreadPoolExecutor( 10, // 平时保持10个活线程 50, // 最多扩到50个线程 60L, TimeUnit.SECONDS, // 空闲线程60秒没人用就关掉 new LinkedBlockingQueue<>(1000), // 任务排队最多1000个 new ThreadFactory() { private int count = 0; @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("deepseek-api-thread-" + count++); thread.setDaemon(true); // 守护线程,程序关的时候不用等它 return thread; } }, new ThreadPoolExecutor.CallerRunsPolicy() // 任务满了就让调用者自己执行,别丢任务 ); } private final DeepSeekTokenManager tokenManager = DeepSeekTokenManager.getInstance(); // 异步调用DeepSeek API(线程安全) public CompletableFuture<String> asyncCall(String prompt) { // 把任务丢到专用线程池里 return CompletableFuture.supplyAsync(() -> { // 关键:每个请求自己建请求头,别共享 HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + tokenManager.getValidToken()); headers.setContentType(MediaType.APPLICATION_JSON); // 关键:每个请求独立创建请求体,避免HashMap线程不安全问题 Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", prompt) )); requestBody.put("temperature", 0.7); // 随机性调中等 requestBody.put("max_tokens", 2000); // 最多返回2000个Token HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); try { ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class); return response.getBody(); } catch (Exception e) { System.err.println("线程" + Thread.currentThread().getName() + "调用出错:" + e.getMessage()); return "{\"choices\":[{\"message\":{\"content\":\"请求失败,稍后再试~\"}}]}"; } }, deepSeekExecutor); } // 关闭线程池(应用关闭时调用) public void shutdownExecutor() { deepSeekExecutor.shutdown(); try { // 等10秒,还没关完就强制关 if (!deepSeekExecutor.awaitTermination(10, TimeUnit.SECONDS)) { deepSeekExecutor.shutdownNow(); } } catch (InterruptedException e) { deepSeekExecutor.shutdownNow(); } }}关键优化点说明连接池限流:控制最大连接数,高并发的时候不会把服务器端口占满;线程池隔离:API调用出问题不会影响核心业务,拒绝策略选“调用者执行”,避免任务丢失;每个请求独立:请求头和请求体都自己建,彻底解决多线程数据乱的问题;异步提升效率:用CompletableFuture异步调用,主线程不用等,服务吞吐量直接上来。三、坑 3:超长文本未分块,触发 API 长度限制问题本质DeepSeek 的模型都有 Token 限制,比如 deepseek-chat 单轮最多大概 8192 个 Token。好多人不管文本多长都直接传,要么返回 400 说“超出长度”,要么自己瞎截断把关键信息切没了,模型回复得乱七八糟。更坑的是,有人按字数算长度,不知道中文和英文占的 Token 不一样,切完还是超。典型错误代码1234567891011121314151617// 错误示例:直接传入超长文本,无分块、无Token计算public class LongTextErrorClient { public String callLongText(String longContent) { Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); // 直接把超长文本丢进去,不管长度 requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", longContent) )); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + DeepSeekTokenManager.getInstance().getValidToken()); HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); RestTemplate restTemplate = new RestTemplate(); return restTemplate.postForEntity(API_URL, request, String.class).getBody(); }}解决方案:Token 计算 + 智能分块 + 结果拼接核心思路:实现 Token 计数器,准确计算文本对应的 Token 数(适配 DeepSeek 的 Token 编码规则);按模型最大 Token 限制,对超长文本进行智能分块(保留语义完整性,避免截断句子);分块调用 API 后,拼接所有分块的回复结果;多轮对话场景下,优先截断历史对话,保留最新上下文。完整正确代码先加依赖(Maven),这个工具能精准算 Token:12345<dependency> <groupId>com.knuddels</groupId> <artifactId>jtokkit</artifactId> <version>1.0.0</version></dependency>然后是长文本处理工具:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139import com.knuddels.jtokkit.Encodings;import com.knuddels.jtokkit.api.Encoding;import com.knuddels.jtokkit.api.EncodingRegistry;import com.knuddels.jtokkit.api.ModelType; import java.util.ArrayList;import java.util.List;import java.util.concurrent.CompletableFuture;import java.util.stream.Collectors; // 长文本处理工具,算Token、分块、拼结果一条龙public class LongTextProcessor { // 初始化Token计算器,DeepSeek用的编码和GPT-4一样 private static final EncodingRegistry registry = Encodings.newDefaultEncodingRegistry(); private static final Encoding encoding = registry.getEncodingForModel(ModelType.GPT_4); // 单轮最大Token数8192,留2000给模型回复,所以请求最多6192个Token private static final int MAX_REQUEST_TOKENS = 8192 - 2000; private final ThreadSafeDeepSeekClient deepSeekClient = new ThreadSafeDeepSeekClient(); // 算文本的Token数,比按字数准多了 public int countTokens(String text) { return encoding.countTokens(text); } // 超长文本分块,尽量不切句子 public List<String> splitLongText(String longText) { List<String> chunks = new ArrayList<>(); int totalTokens = countTokens(longText); // 没超限制就直接返回 if (totalTokens <= MAX_REQUEST_TOKENS) { chunks.add(longText); return chunks; } // 按中文和英文的句号分割句子,保持意思完整 String[] sentences = longText.split("(?<=[。!?.?!])"); StringBuilder currentChunk = new StringBuilder(); int currentTokens = 0; for (String sentence : sentences) { int sentenceTokens = countTokens(sentence); // 极端情况:一个句子就超了,那就按Token硬切 if (sentenceTokens > MAX_REQUEST_TOKENS) { chunks.addAll(splitOverLengthSentence(sentence, MAX_REQUEST_TOKENS)); continue; } // 加当前句子会超的话,先把之前的存起来 if (currentTokens + sentenceTokens > MAX_REQUEST_TOKENS) { chunks.add(currentChunk.toString().trim()); currentChunk = new StringBuilder(); currentTokens = 0; } currentChunk.append(sentence); currentTokens += sentenceTokens; } // 把最后一块加进去 if (currentChunk.length() > 0) { chunks.add(currentChunk.toString().trim()); } return chunks; } // 单个句子超Token限制,按Token硬切 private List<String> splitOverLengthSentence(String sentence, int maxTokens) { List<String> subChunks = new ArrayList<>(); char[] chars = sentence.toCharArray(); StringBuilder subChunk = new StringBuilder(); int currentTokens = 0; for (char c : chars) { String charStr = String.valueOf(c); int charTokens = countTokens(charStr); if (currentTokens + charTokens > maxTokens) { subChunks.add(subChunk.toString().trim()); subChunk = new StringBuilder(); currentTokens = 0; } subChunk.append(c); currentTokens += charTokens; } if (subChunk.length() > 0) { subChunks.add(subChunk.toString().trim()); } return subChunks; } // 分块调用API,最后拼结果 public String processLongText(String longText) { List<String> chunks = splitLongText(longText); // 就一块的话直接调 if (chunks.size() == 1) { return deepSeekClient.asyncCall(chunks.get(0)).join(); } // 多块的话异步调用,效率高 List<CompletableFuture<String>> futures = chunks.stream() .map(chunk -> { // 告诉模型这是第几块,总共多少块,让它有上下文 String prompt = "请处理以下文本片段(一共" + chunks.size() + "段,这是第" + (chunks.indexOf(chunk) + 1) + "段):\n" + chunk; return deepSeekClient.asyncCall(prompt); }) .collect(Collectors.toList()); // 等所有调用都完成 CompletableFuture<Void> allDone = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); allDone.join(); // 拼结果 StringBuilder finalResult = new StringBuilder(); finalResult.append("超长文本处理结果(按片段拼接):\n"); for (CompletableFuture<String> future : futures) { try { String chunkRes = future.get(); // 提取回复内容,实际项目里用FastJSON或者Jackson解析更靠谱 String content = extractContent(chunkRes); finalResult.append(content).append("\n"); } catch (Exception e) { finalResult.append("【这段处理失败】:").append(e.getMessage()).append("\n"); } } return finalResult.toString(); } // 从API响应里把回复内容抠出来(简化版,实际用JSON库) private String extractContent(String response) { int contentStart = response.indexOf("\"content\":\"") + 10; int contentEnd = response.indexOf("\"}", contentStart); if (contentStart > 0 && contentEnd > contentStart) { return response.substring(contentStart, contentEnd); } return response; }}关键优化点说明精准Token计算:使用 jtokkit 库(适配 OpenAI/DeepSeek 的 Token 编码规则),准确计算文本 Token 数,避免按字符数分割导致的误差;语义化分块:优先按句子分割,保留文本语义完整性,避免截断导致的上下文丢失;极端情况处理:单个句子超出 Token 限制时,按 Token 数切割,保证分块后能正常调用 API;异步分块调用:多块文本异步调用 API,提高处理效率,最后拼接结果;上下文标识:给每个分块添加段数标识,让模型理解当前处理的是超长文本的一部分,提升回复质量。四、坑 4:模型名称配置错误问题本质不同模型的名称规范不同,若将其他模型的名称直接套用在 DeepSeek 上,会返回 404 错误(模型不存在)。解决方案搞个枚举类存DeepSeek的模型名:12345678910111213141516171819202122232425262728293031323334// DeepSeek模型名枚举,直接拿过来用public class DeepSeekModelEnum { // 通用对话、代码生成、推理增强,常用的就这三个 public static final String DEEPSEEK_CHAT = "deepseek-chat"; public static final String DEEPSEEK_CODER = "deepseek-coder"; public static final String DEEPSEEK_R1 = "deepseek-r1"; // 如果你之前用别的模型,用这个方法转成DeepSeek的 public static String convertFromOtherModel(String otherModelName) { switch (otherModelName.toLowerCase()) { // 之前用其他AI的对话场景,转成deepseek-chat case "gpt-3.5-turbo": case "ernie-bot": return DEEPSEEK_CHAT; // 之前用代码模型的,转成deepseek-coder case "code-davinci-002": return DEEPSEEK_CODER; // 其他情况默认用通用对话模型 default: return DEEPSEEK_CHAT; } }} // 调用示例public class ModelClient { public String callWithRightModel(String prompt) { Map<String, Object> requestBody = new HashMap<>(); // 直接用枚举里的模型名,肯定不会错 requestBody.put("model", DeepSeekModelEnum.DEEPSEEK_CHAT); // 其他参数... return ""; }}五、坑 5:响应参数解析错误问题本质虽然DeepSeek响应格式和OpenAI像,但有些字段不一样,比如finish_reason的取值、usage里的统计方式。有人直接抄其他AI的解析代码,结果要么字段拿不到,要么报解析异常。解决方案搞个专门的解析工具,兼容这些差异:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject; // DeepSeek响应解析工具类(适配字段差异)public class DeepSeekResponseParser { // 提取回复内容 public static String getContent(String responseJson) { try { JSONObject root = JSON.parseObject(responseJson); // 兼容DeepSeek和OpenAI的响应结构 if (root.containsKey("choices") && !root.getJSONArray("choices").isEmpty()) { JSONObject choice = root.getJSONArray("choices").getJSONObject(0); // DeepSeek的message字段与OpenAI一致,但需判空 if (choice.containsKey("message")) { return choice.getJSONObject("message").getString("content"); } } // 要是有error字段,直接抛异常 if (root.containsKey("error")) { throw new RuntimeException("API报错:" + root.getJSONObject("error").getString("message")); } return "没拿到有效回复"; } catch (Exception e) { throw new RuntimeException("解析响应失败:" + e.getMessage()); } } // 解析Token使用量(适配DeepSeek的usage字段) public static int getUsedTokens(String responseJson) { JSONObject root = JSON.parseObject(responseJson); if (root.containsKey("usage")) { return root.getJSONObject("usage").getIntValue("total_tokens"); } return 0; }} // 调用示例public class ParserClient { public void parseResponse(String response) { // 直接拿回复内容,不用自己处理JSON String content = DeepSeekResponseParser.getContent(response); // 看看用了多少Token int usedTokens = DeepSeekResponseParser.getUsedTokens(response); System.out.println("回复:" + content); System.out.println("消耗Token:" + usedTokens); }}六、坑 6:超时配置不匹配问题本质DeepSeek API 的响应速度与模型类型、文本长度相关(如 deepseek-coder 处理代码时响应较慢),若直接复用其他AI的超时配置(如 10 秒),会导致频繁超时;反之,超时配置过长会导致线程阻塞。解决方案123456789101112131415161718192021222324252627282930313233343536373839404142434445// 动态超时配置工具public class TimeoutConfig { // 不同模型的基础超时时间(毫秒) private static final int CHAT_TIMEOUT = 30000; // 对话模型30秒 private static final int CODER_TIMEOUT = 60000; // 代码模型60秒 private static final int R1_TIMEOUT = 45000; // 推理模型45秒 // 按模型拿基础超时 public static int getBaseTimeout(String modelName) { switch (modelName) { case DeepSeekModelEnum.DEEPSEEK_CODER: return CODER_TIMEOUT; case DeepSeekModelEnum.DEEPSEEK_R1: return R1_TIMEOUT; default: return CHAT_TIMEOUT; } } // 按文本长度加超时,长文本给更多时间 public static int getDynamicTimeout(String modelName, String text) { int baseTimeout = getBaseTimeout(modelName); int textLen = text.length(); // 每1000字加5秒,最多不超过基础超时的2倍(别无限加) int extraTimeout = Math.min((textLen / 1000) * 5000, baseTimeout); return baseTimeout + extraTimeout; }} // 调用示例public class TimeoutClient { public String callWithDynamicTimeout(String prompt) { String model = DeepSeekModelEnum.DEEPSEEK_CODER; // 按模型和文本长度算超时 int timeout = TimeoutConfig.getDynamicTimeout(model, prompt); Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", model); requestBody.put("messages", Collections.singletonList(Map.of("role", "user", "content", prompt))); requestBody.put("timeout", timeout); // 把算好的超时传进去 // 其他请求逻辑... return ""; }}七、坑 7:请求参数不兼容问题本质有些参数在别的模型里有用,但DeepSeek不支持。解决方案123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import java.util.HashMap;import java.util.List;import java.util.Map; // 请求参数适配工具public class ParamsAdapter { // DeepSeek不支持的参数列表 private static final List<String> UNSUPPORTED_PARAMS = List.of( "frequency_penalty", "presence_penalty", "logit_bias" ); // 适配参数,保证传过去的都能用 public static Map<String, Object> adapt(Map<String, Object> originalParams) { Map<String, Object> adaptedParams = new HashMap<>(originalParams); // 1. 把不支持的参数删掉 UNSUPPORTED_PARAMS.forEach(adaptedParams::remove); // 2. 修正temperature:只能0-1 if (adaptedParams.containsKey("temperature")) { double temp = (double) adaptedParams.get("temperature"); // 小于0取0,大于1取1,中间的不变 adaptedParams.put("temperature", Math.min(Math.max(temp, 0.0), 1.0)); } // 3. 修正max_tokens:最少10,最多4096 if (adaptedParams.containsKey("max_tokens")) { int maxTokens = (int) adaptedParams.get("max_tokens"); adaptedParams.put("max_tokens", Math.min(Math.max(maxTokens, 10), 4096)); } return adaptedParams; }} // 调用示例public class ParamsClient { public String callWithRightParams(String prompt) { // 原来的参数,可能有无效的 Map<String, Object> originalParams = new HashMap<>(); originalParams.put("model", DeepSeekModelEnum.DEEPSEEK_CHAT); originalParams.put("temperature", 1.5); // 超出DeepSeek的范围 originalParams.put("frequency_penalty", 0.5); // DeepSeek不支持 originalParams.put("messages", Collections.singletonList(Map.of("role", "user", "content", prompt))); // 适配后再传 Map<String, Object> adaptedParams = ParamsAdapter.adapt(originalParams); // 其他请求逻辑... return ""; }}八、坑 8:错误码处理不兼容问题本质同样是429错误,DeepSeek表示“请求太频繁,触发限流了”,但文心一言可能表示“Token用完了”;还有500错误,有人以为是自己代码的问题,其实是DeepSeek服务端的问题,白查半天。解决方案1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192import org.springframework.web.client.HttpClientErrorException; // 错误码适配工具类public class DeepSeekErrorHandler { // DeepSeek常见错误码说明 public enum ErrorCode { TOKEN_EXPIRED(401, "Token过期或无效,该刷新了"), RATE_LIMIT(429, "请求太频繁,歇会儿再试"), TOO_LONG(400, "文本太长,超过Token限制了"), MODEL_NOT_FOUND(404, "模型名填错了"), SERVER_ERROR(500, "DeepSeek服务端出问题了"); private final int code; private final String desc; ErrorCode(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } } // 处理DeepSeek错误,返回适配的重试策略 public static boolean needRetry(Exception e) { if (e instanceof HttpClientErrorException) { int statusCode = ((HttpClientErrorException) e).getStatusCode().value(); // 限流和服务端错误可以重试,其他的别瞎试 return statusCode == ErrorCode.RATE_LIMIT.getCode() || statusCode == ErrorCode.SERVER_ERROR.getCode(); } // 网络超时、连接失败这些也可以重试 return e instanceof java.net.SocketTimeoutException || e instanceof java.net.ConnectException; } // 获取错误描述,适配不同模型的错误码 public static String getErrorMsg(Exception e) { if (e instanceof HttpClientErrorException) { HttpClientErrorException ex = (HttpClientErrorException) e; int statusCode = ex.getStatusCode().value(); // 匹配错误码 for (ErrorCode errorCode : ErrorCode.values()) { if (errorCode.getCode() == statusCode) { return errorCode.getDesc() + ",详情:" + ex.getResponseBodyAsString(); } } } // 其他错误直接返回信息 return "未知错误:" + e.getMessage(); }} // 调用示例,带重试逻辑public class RetryClient { private static final int MAX_RETRY = 3; // 最多重试3次 public String callWithRetry(String prompt) { int retryCount = 0; while (retryCount < MAX_RETRY) { try { // 调用API的逻辑 DeepSeekApiClient client = new DeepSeekApiClient(); return client.callDeepSeek(prompt); } catch (Exception e) { retryCount++; String errorMsg = DeepSeekErrorHandler.getErrorMsg(e); System.err.println("第" + retryCount + "次调用失败:" + errorMsg); // 不该重试就直接退出 if (!DeepSeekErrorHandler.needRetry(e)) { break; } // 指数退避重试:第1次等2秒,第2次等4秒,第3次等8秒 try { Thread.sleep((long) (Math.pow(2, retryCount) * 1000)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } } return "{\"choices\":[{\"message\":{\"content\":\"试了好几次都不行,稍后再试吧~\"}}]}"; }}九、总结与最佳实践本文拆解了 Java 调用 DeepSeek API 的 8 个高频错误,从 Token 管理、并发安全、超长文本处理到跨模型适配,核心避坑思路可总结为:1. 鉴权层:主动防御 + 被动兜底避免硬编码 Token,对接配置中心实现动态刷新;结合主动刷新(提前检测有效期)和被动刷新(捕获 401 异常),保证 Token 有效性;增加降级策略,Token 刷新失败时临时延长有效期,避免服务中断。2. 并发层:隔离 + 安全配置 HTTP 连接池,限制最大连接数,避免端口耗尽;使用专用线程池隔离 API 调用,合理设置核心线程数、队列大小和拒绝策略;每个请求独立创建非线程安全对象(如 HttpHeaders、HashMap),杜绝数据错乱。3. 文本层:精准计算 + 语义分块使用专业 Token 计算库,避免按字符数分割导致的误差;优先按句子分块,保留语义完整性,极端情况按 Token 切割;分块调用时添加上下文标识,提升回复质量,最后拼接结果。4. 适配层:兼容差异 + 动态调整按模型类型适配超时时间、请求参数,过滤不支持的参数;适配错误码处理逻辑,针对不同错误码制定差异化重试策略;解析响应时兼容字段差异,避免解析异常。
-
一、需求分析与规划1. 功能需求系统需满足三类核心用户(教学管理员、教师、学生)的差异化需求,功能拆解如下:教学管理员端:用户管理(新增教师/学生账号、分配班级与课程权限)、课程管理(创建课程、关联授课教师)、成绩模板配置(设置成绩构成比例,如平时成绩占30%、期末成绩占70%)、成绩数据导出(按班级/课程/学期生成Excel报表)、系统日志查看(跟踪成绩修改、账号操作记录);教师端:成绩录入(按学生名单批量或单个录入平时、期中、期末成绩,系统自动计算总成绩)、成绩审核(提交总成绩前预览核对,提交后锁定不可修改)、成绩分析(查看所授课程的平均分、及格率、分数段分布图表)、学生成绩反馈(回复学生的成绩疑问,标注异常成绩说明);学生端:成绩查询(按学期/课程查看个人各项成绩及总成绩)、成绩趋势分析(查看同一课程历年成绩对比或个人多学期成绩变化)、成绩异议申请(对疑问成绩提交申诉,跟踪处理进度)、成绩单下载(下载经管理员审核后的官方成绩证明)。2. 核心模块基于需求拆解,系统划分为5个联动模块,模块职责与关联关系如下表所示:模块名称 核心功能 关联模块用户权限模块 账号注册登录、角色权限分配(管理员/教师/学生)、密码重置、账号状态管理 所有模块(权限校验)课程管理模块 课程信息维护(新增/编辑/删除课程)、班级-课程-教师关联、课程学期管理 用户权限模块(教师课程权限)、成绩管理模块(课程成绩归属)成绩管理模块 成绩录入与计算、成绩审核与锁定、成绩修改申请与审批、异常成绩标注 课程管理模块(课程数据)、用户权限模块(成绩操作权限)数据统计模块 成绩分析(平均分、及格率、分数段)、成绩趋势图表生成、数据报表导出 成绩管理模块(成绩数据源)、课程管理模块(课程筛选条件)消息通知模块 成绩录入提醒、成绩审核结果推送、学生成绩异议反馈、系统公告发布 成绩管理模块(成绩状态变更)、用户权限模块(接收人匹配)二、飞算JavaAI开发实录1. 步骤1:输入提示词(明确开发需求)打开IntelliJ IDEA并启动飞算JavaAI插件,在“需求输入”界面输入提示词:“开发学生成绩管理系统后端代码,基于Spring Boot 3.0和MySQL 8.0,需包含用户权限(管理员/教师/学生三类角色)、课程管理(课程CRUD与师生关联)、成绩管理(成绩录入/计算/审核)三大核心模块,生成实体类、Mapper接口、Service层、Controller层代码,符合RESTful API规范,需支持成绩自动计算(按比例汇总)与权限细粒度控制(如教师仅能操作所授课程成绩)。” 2. 步骤2:AI理解需求(需求拆解与确认)提交提示词后,飞算JavaAI约8秒生成需求拆解报告,核心内容包括:需求匹配度分析:明确系统为教学场景下的成绩管理类系统,核心诉求是“数据安全+效率提升”,需重点解决权限隔离与成绩自动化处理,与教育领域常见管理系统需求高度适配;模块拆解确认:将需求细化为用户权限模块(含角色权限矩阵设计,如管理员全权限、教师仅课程内权限)、课程管理模块(含班级-课程-教师关联逻辑)、成绩管理模块(含成绩计算规则配置、审核流程设计)。3. 步骤3:设计接口(RESTful API规划)插件自动跳转至“接口设计”界面,围绕三大核心模块生成接口说明,明确各接口功能定位:用户权限接口:负责账号认证(登录生成JWT令牌)、权限校验(接口访问时验证角色权限)、用户信息维护,例如/api/user/login(登录接口)、/api/user/role/assign(权限分配接口);课程管理接口:实现课程信息CRUD与关联管理,例如/api/course/add(新增课程)、/api/course/teacher/bind(课程-教师绑定接口);成绩管理接口:覆盖成绩录入、计算、审核全流程,例如/api/score/batch/save(批量录入成绩)、/api/score/calculate(自动计算总成绩接口)、/api/score/audit(成绩审核接口)。4. 步骤4:表结构设计(数据库表生成)接口设计完成后,插件自动推导数据库表结构(支持手动编辑),核心表结构如下:CREATE TABLE user_info ( user_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户唯一标识', username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', password VARCHAR(255) NOT NULL COMMENT '密码(加密存储)', user_role ENUM('admin', 'teacher', 'student') NOT NULL COMMENT '用户角色:admin-管理员,teacher-教师,student-学生', status TINYINT DEFAULT 1 COMMENT '用户状态:1-正常,0-禁用', create_by VARCHAR(50) COMMENT '创建人', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_by VARCHAR(50) COMMENT '修改人', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间') COMMENT='用户信息表';CREATE TABLE course_info ( course_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '课程唯一标识', course_name VARCHAR(100) NOT NULL COMMENT '课程名称', course_code VARCHAR(50) NOT NULL UNIQUE COMMENT '课程编号', teacher_id BIGINT COMMENT '授课教师ID', description TEXT COMMENT '课程描述', credits INT DEFAULT 0 COMMENT '学分', status TINYINT DEFAULT 1 COMMENT '课程状态:1-启用,0-停用', create_by VARCHAR(50) COMMENT '创建人', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_by VARCHAR(50) COMMENT '修改人', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间') COMMENT='课程信息表';CREATE TABLE student_course ( relation_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '师生关联关系唯一标识', student_id BIGINT NOT NULL COMMENT '学生用户ID', course_id BIGINT NOT NULL COMMENT '课程ID', select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', create_by VARCHAR(50) COMMENT '创建人', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间') COMMENT='学生选课关系表';CREATE TABLE score_record ( score_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '成绩记录唯一标识', student_id BIGINT NOT NULL COMMENT '学生用户ID', course_id BIGINT NOT NULL COMMENT '课程ID', score_type ENUM('exam', 'homework', 'project', 'quiz') NOT NULL COMMENT '成绩类型:exam-考试,homework-作业,project-项目,quiz-小测', score_value DECIMAL(5,1) NOT NULL COMMENT '成绩数值', score_weight DECIMAL(3,2) DEFAULT 1.00 COMMENT '该成绩在总评中的权重比例', is_reviewed TINYINT DEFAULT 0 COMMENT '是否已审核:0-未审核,1-已审核', review_time DATETIME COMMENT '审核时间', reviewer VARCHAR(50) COMMENT '审核人', create_by VARCHAR(50) COMMENT '创建人', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_by VARCHAR(50) COMMENT '修改人', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间') COMMENT='成绩记录表';一键获取完整项目代码bash5. 步骤5:处理逻辑(接口实现思路生成)进入“处理逻辑”界面,插件针对核心接口生成详细业务流程,以“成绩管理”模块为例,关键接口逻辑如下:接口名称: 录入成绩入参对象属性:studentId(学生ID,必填)、courseId(课程ID,必填)、score(成绩分数,必填)、type(成绩类型,必填)处理逻辑:检查学生是否选课且课程是否存在返回RestResult结果:{“code”:“000001”,“msg”:“学生未选该课程或课程不存在”,“data”:…}处理逻辑:将成绩信息保存到数据库返回RestResult结果:{“code”:“000000”,“msg”:“调用成功”,“data”:…}接口名称:计算并汇总成绩入参对象属性:studentId(学生ID,必填)、courseId(课程ID,必填)处理逻辑:获取该学生在指定课程下的所有成绩项返回RestResult结果:{“code”:“000001”,“msg”:“无相关成绩记录”,“data”:…}处理逻辑:根据预设比例规则进行加权计算返回RestResult结果:{“code”:“000000”,“msg”:“调用成功”,“data”:…}接口名称:审核成绩入参对象属性:studentId(学生ID,必填)、courseId(课程ID,必填)、status(审核状态,必填)处理逻辑:确认成绩是否存在且处于待审核状态返回RestResult结果:{“code”:“000001”,“msg”:“成绩不存在或已审核”,“data”:…}处理逻辑:更新成绩的审核状态为通过或拒绝返回RestResult结果:{“code”:“000000”,“msg”:“调用成功”,“data”:…}接口名称:修改成绩入参对象属性:studentId(学生ID,必填)、courseId(课程ID,必填)、newScore(新成绩分数,必填)处理逻辑:验证成绩是否允许修改(如:是否已审核)返回RestResult结果:{“code”:“000001”,“msg”:“成绩不可修改”,“data”:…}处理逻辑:更新对应的成绩数据返回RestResult结果:{“code”:“000000”,“msg”:“调用成功”,“data”:…}接口名称:查看成绩历史记录入参对象属性:studentId(学生ID,必填)、courseId(课程ID,必填)处理逻辑:查询该学生在指定课程下所有的成绩变更记录返回RestResult结果:{“code”:“000000”,“msg”:“调用成功”,“data”:…}6. 步骤6:生成源码(完整工程代码输出)确认处理逻辑后,点击“生成源码”,飞算JavaAI约3分钟生成完整工程代码(遵循MVC架构),核心代码结构如下:配置类:src/main/java/score/system/config/CorsConfig(跨域配置)、RedisConfig(Redis缓存配置)、SecurityConfig(权限安全配置);Controller层:src/main/java/score/system/controller/UserController.java、CourseController.java、ScoreController.java(接口映射与参数接收);DTO类:src/main/java/score/system/dto/request/ScoreBatchSaveRequest.java(批量成绩录入请求类)、ScoreAuditRequest.java(成绩审核请求类),src/main/java/score/system/dto/response/ScoreStatisticResponse.java(成绩统计响应类);实体类:src/main/java/score/system/entity/UserInfo.java、CourseInfo.java、ScoreInfo.java、ScoreRule.java(映射数据库表);Service层:src/main/java/score/system/service/UserService.java、CourseService.java、ScoreService.java(接口定义)及对应的impl实现类(业务逻辑实现);启动类:src/main/java/score/system/ScoreManagementApplication.java(项目入口)。生成的代码无语法错误,导入IDEA后可直接启动测试。三、优化与调试心得1. 遇到的问题及解决方案(1)问题1:成绩计算精度丢失生成的ScoreService中,总成绩计算使用普通浮点数运算,存在精度丢失问题(如30%平时成绩+70%期末成绩,结果出现多位小数)。解决方案:通过飞算JavaAI“智能会话”查询“Java BigDecimal精确计算成绩比例”,获取优化代码,在ScoreServiceImpl中使用BigDecimal类进行比例计算,示例如下:BigDecimal usualRatio = new BigDecimal(scoreRule.getUsualRatio()).divide(new BigDecimal(100));BigDecimal midtermRatio = new BigDecimal(scoreRule.getMidtermRatio()).divide(new BigDecimal(100));BigDecimal finalRatio = new BigDecimal(scoreRule.getFinalRatio()).divide(new BigDecimal(100));BigDecimal totalScore = usualScore.multiply(usualRatio) .add(midtermScore.multiply(midtermRatio)) .add(finalScore.multiply(finalRatio)) .setScale(2, BigDecimal.ROUND_HALF_UP);一键获取完整项目代码java(2)问题2:高频成绩查询性能低学生集中查询成绩时,频繁访问MySQL数据库,导致接口响应延迟(平均响应时间超1.5秒)。解决方案:借助飞算JavaAI“智能会话”获取“Spring Boot Redis缓存成绩数据”方案,在ScoreService的成绩查询方法上添加@Cacheable注解,缓存课程成绩统计结果与学生个人成绩,示例:@Cacheable(value = "score:student", key = "#studentId + ':' + #courseId")public ScoreInfo getStudentCourseScore(Long studentId, Long courseId) { return scoreMapper.selectByStudentAndCourse(studentId, courseId);}一键获取完整项目代码java优化后接口响应时间降至0.3秒以内。(3)问题3:成绩修改无痕迹教师提交成绩后,若管理员驳回需修改,但生成的代码未记录成绩修改历史,无法追溯变更内容。解决方案:手动新增score_history(成绩修改历史表),并通过飞算JavaAI生成ScoreHistory实体与对应的Mapper、Service代码,在ScoreServiceImpl的update方法中添加历史记录逻辑,每次修改成绩时自动保存旧数据至score_history表,实现“每改必留痕”。四、系统成果展示与应用价值1. 功能成果展示系统开发完成后,通过本地部署与多角色实测,实现了 “需求全覆盖、操作零门槛、数据高可靠” 的开发目标,核心功能代码成果如下:package com.example.service.impl;import com.example.demo1.dto.request.*;import com.example.dto.request.BoundTeacherToCourseRequest;import com.example.dto.request.CreateCourseRequest;import com.example.dto.request.DeleteCourseRequest;import com.example.dto.request.SelectCourseRequest;import com.example.dto.request.UpdateCourseRequest;import com.example.dto.response.RestResult;import com.example.entity.CourseInfo;import com.example.entity.StudentCourse;import com.example.entity.UserInfo;import com.example.repository.CourseRepository;import com.example.repository.StudentCourseRepository;import com.example.repository.UserRepository;import com.example.service.CourseService;import java.time.LocalDateTime;import java.util.List;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageImpl;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Pageable;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;/** * 课程服务实现类 * */@Slf4j@Service@Transactionalpublic class CourseServiceImpl implements CourseService { @Autowired private CourseRepository courseRepository; @Autowired private UserRepository userRepository; @Autowired private StudentCourseRepository studentCourseRepository; @Override public RestResult<Object> createCourse(CreateCourseRequest request) { // 校验课程名称是否已存在 if (courseRepository.findByCourseName(request.getCourseName()) != null) { return RestResult.error("000001", "课程名称已存在"); } // 构造新的课程信息对象 CourseInfo courseInfo = new CourseInfo(); BeanUtils.copyProperties(request, courseInfo); courseInfo.setCreateTime(LocalDateTime.now()); courseInfo.setUpdateTime(LocalDateTime.now()); courseInfo.setStatus((byte) 1); // 默认启用 try { courseRepository.save(courseInfo); return RestResult.success(courseInfo); } catch (Exception e) { log.error("创建课程失败", e); return RestResult.error("999999", "系统错误"); } } @Override public RestResult<Object> queryCourseList(Integer pageNo, Integer pageSize, String keyword) { // 设置默认值 if (pageNo == null || pageNo <= 0) { pageNo = 1; } if (pageSize == null || pageSize <= 0) { pageSize = 10; } Pageable pageable = PageRequest.of(pageNo - 1, pageSize); List<CourseInfo> courses = courseRepository.findAll(pageable).getContent(); Page<CourseInfo> coursePage = new PageImpl<>(courses, pageable, courseRepository.count()); return RestResult.success(coursePage); } @Override public RestResult<Object> updateCourse(UpdateCourseRequest request) { // 判断课程是否存在 CourseInfo existingCourse = courseRepository.findById(request.getCourseId()).orElse(null); if (existingCourse == null) { return RestResult.error("000001", "课程不存在"); } // 如果传了课程名称,则检查是否与其他课程冲突 if (request.getCourseName() != null && !request.getCourseName().equals(existingCourse.getCourseName())) { if (courseRepository.findByCourseName(request.getCourseName()) != null) { return RestResult.error("000001", "课程名称已存在"); } } // 更新课程信息 BeanUtils.copyProperties(request, existingCourse, "courseId", "createTime"); // 忽略这些字段 existingCourse.setUpdateTime(LocalDateTime.now()); try { courseRepository.save(existingCourse); return RestResult.success(existingCourse); } catch (Exception e) { log.error("更新课程失败", e); return RestResult.error("999999", "系统错误"); } } @Override public RestResult<Object> deleteCourse(DeleteCourseRequest request) { // 判断课程是否存在 CourseInfo existingCourse = courseRepository.findById(request.getCourseId()).orElse(null); if (existingCourse == null) { return RestResult.error("000001", "课程不存在"); } try { // 删除相关的学生选课记录 studentCourseRepository.deleteByCourseId(request.getCourseId()); // 删除课程本身 courseRepository.deleteById(request.getCourseId()); return RestResult.success(null); } catch (Exception e) { log.error("删除课程失败", e); return RestResult.error("999999", "系统错误"); } } @Override public RestResult<Object> boundTeacherToCourse(BoundTeacherToCourseRequest request) { // 判断教师与课程是否存在 UserInfo teacher = userRepository.findById(request.getTeacherId()).orElse(null); CourseInfo course = courseRepository.findById(request.getCourseId()).orElse(null); if (teacher == null || course == null) { return RestResult.error("000001", "教师或课程不存在"); } // 更新课程中的教师ID course.setTeacherId(request.getTeacherId()); course.setUpdateTime(LocalDateTime.now()); try { courseRepository.save(course); return RestResult.success(null); } catch (Exception e) { log.error("绑定教师到课程失败", e); return RestResult.error("999999", "系统错误"); } } @Override public RestResult<Object> selectCourse(SelectCourseRequest request) { // 判断学生和课程是否存在 UserInfo student = userRepository.findById(request.getStudentId()).orElse(null); CourseInfo course = courseRepository.findById(request.getCourseId()).orElse(null); if (student == null || course == null) { return RestResult.error("000001", "学生或课程不存在"); } // 检查该学生是否已经选过此课程 StudentCourse existingRecord = studentCourseRepository.findByStudentIdAndCourseId(request.getStudentId(), request.getCourseId()); if (existingRecord != null) { return RestResult.error("000001", "该学生已选过此课程"); } // 添加学生的选课记录 StudentCourse record = new StudentCourse(); record.setStudentId(request.getStudentId()); record.setCourseId(request.getCourseId()); record.setCreateTime(LocalDateTime.now()); try { studentCourseRepository.save(record); return RestResult.success(null); } catch (Exception e) { log.error("学生选课失败", e); return RestResult.error("999999", "系统错误"); } }}一键获取完整项目代码plainpackage com.example.service.impl;import com.example.dto.request.ScoreModifyRequest;import com.example.dto.request.ScoreRecordRequest;import com.example.dto.request.ScoreReviewRequest;import com.example.entity.CourseInfo;import com.example.entity.ScoreRecord;import com.example.entity.StudentCourse;import com.example.repository.CourseRepository;import com.example.repository.ScoreRepository;import com.example.repository.StudentCourseRepository;import com.example.service.ScoreService;import com.example.util.RestResult;import java.math.BigDecimal;import java.time.LocalDateTime;import java.util.List;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;/** * 成绩服务实现类 */@Slf4j@Service@Transactionalpublic class ScoreServiceImpl implements ScoreService { @Autowired private ScoreRepository scoreRepository; @Autowired private CourseRepository courseRepository; @Autowired private StudentCourseRepository studentCourseRepository; /** * 录入成绩 * * @param request 入参对象 * @return RestResult 结果 */ @Override public RestResult recordScore(ScoreRecordRequest request) { // 检查学生是否选课且课程是否存在 if (!checkStudentSelectCourse(request.getStudentId(), request.getCourseId())) { return RestResult.error("学生未选该课程或课程不存在"); } try { ScoreRecord scoreRecord = new ScoreRecord(); scoreRecord.setStudentId(request.getStudentId()); scoreRecord.setCourseId(request.getCourseId()); scoreRecord.setScoreType(request.getType()); scoreRecord.setScoreValue(BigDecimal.valueOf(request.getScore())); scoreRecord.setIsReviewed(0); // 默认未审核 scoreRecord.setCreateTime(LocalDateTime.now()); scoreRecord.setUpdateTime(LocalDateTime.now()); scoreRepository.save(scoreRecord); return RestResult.success("录入成功"); } catch (Exception e) { log.error("录入成绩失败: ", e); return RestResult.error("系统异常,请稍后再试"); } } /** * 计算并汇总成绩 * * @param studentId 学生ID * @param courseId 课程ID * @return RestResult 结果 */ @Override public RestResult calculateScore(Long studentId, Long courseId) { List<ScoreRecord> records = scoreRepository.findByStudentIdAndCourseIdOrderByCreateTimeAsc(studentId, courseId); if (records.isEmpty()) { return RestResult.error("无相关成绩记录"); } double totalScore = 0.0; for (ScoreRecord record : records) { if (record.getIsReviewed() == 1) { // 只计算已审核的成绩 totalScore += record.getScoreValue().doubleValue() * record.getScoreWeight().doubleValue(); } } return RestResult.success(totalScore); } /** * 审核成绩 * * @param request 入参对象 * @return RestResult 结果 */ @Override public RestResult reviewScore(ScoreReviewRequest request) { ScoreRecord latestRecord = scoreRepository.findLatestByStudentIdAndCourseId(request.getStudentId(), request.getCourseId()); if (latestRecord == null || latestRecord.getIsReviewed() == 1) { return RestResult.error("成绩不存在或已审核"); } try { latestRecord.setIsReviewed(request.getStatus() ? 1 : 0); latestRecord.setReviewer("system"); // 这里可以替换为当前登录用户的用户名 latestRecord.setReviewTime(LocalDateTime.now()); latestRecord.setUpdateTime(LocalDateTime.now()); scoreRepository.save(latestRecord); return RestResult.success("审核完成"); } catch (Exception e) { log.error("审核成绩失败: ", e); return RestResult.error("系统异常,请稍后再试"); } } /** * 修改成绩 * * @param request 入参对象 * @return RestResult 结果 */ @Override public RestResult modifyScore(ScoreModifyRequest request) { ScoreRecord latestRecord = scoreRepository.findLatestByStudentIdAndCourseId(request.getStudentId(), request.getCourseId()); if (latestRecord == null) { return RestResult.error("成绩不存在"); } // 判断是否已经审核过 if (latestRecord.getIsReviewed() == 1) { return RestResult.error("成绩不可修改"); } try { latestRecord.setScoreValue(BigDecimal.valueOf(request.getNewScore())); latestRecord.setUpdateTime(LocalDateTime.now()); scoreRepository.save(latestRecord); return RestResult.success("修改成功"); } catch (Exception e) { log.error("修改成绩失败: ", e); return RestResult.error("系统异常,请稍后再试"); } } /** * 查看成绩历史记录 * * @param studentId 学生ID * @param courseId 课程ID * @return RestResult 结果 */ @Override public RestResult getScoreHistory(Long studentId, Long courseId) { List<ScoreRecord> records = scoreRepository.findByStudentIdAndCourseIdOrderByCreateTimeAsc(studentId, courseId); return RestResult.success(records); } /** * 检查学生是否选课且课程是否存在 * * @param studentId 学生ID * @param courseId 课程ID * @return boolean 是否满足条件 */ private boolean checkStudentSelectCourse(Long studentId, Long courseId) { // 检查课程是否存在且启用 CourseInfo course = courseRepository.findById(courseId).orElse(null); if (course == null || course.getStatus() != 1) { return false; } // 检查学生是否选了这门课 StudentCourse studentCourse = studentCourseRepository.findOneByStudentIdAndCourseId(studentId, courseId); return studentCourse != null; }}一键获取完整项目代码plainpackage com.example.service.impl;import com.example.demo1.entity.UserRole;import com.example.dto.request.LoginRequest;import com.example.dto.request.RegisterRequest;import com.example.dto.request.RoleAssignRequest;import com.example.dto.response.RestResult;import com.example.entity.UserInfo;import com.example.repository.UserRepository;import com.example.service.UserService;import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import java.security.Key;import java.time.LocalDateTime;import java.util.Date;import java.util.HashMap;import java.util.Map;import javax.crypto.spec.SecretKeySpec;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import org.springframework.util.StringUtils;/** * 用户业务逻辑实现类 */@Slf4j@Servicepublic class UserServiceImpl implements UserService { private final UserRepository userRepository; // JWT密钥,实际应用中应从配置文件读取 @Value("${jwt.secret}") private String secretKey; public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public RestResult<?> register(RegisterRequest request) { log.info("开始进行用户注册: {}", request.getUsername()); if (StringUtils.hasText(request.getUsername()) && StringUtils.hasText(request.getPassword())) { // 检查用户是否已存在 if (userRepository.findByUsername(request.getUsername()).isPresent()) { return RestResult.fail("000001", "用户名已存在"); } // 将新用户信息保存到数据库 try { UserInfo userInfo = new UserInfo(); userInfo.setUsername(request.getUsername()); userInfo.setPassword(encryptPassword(request.getPassword())); // 假设有一个密码加密方法 userInfo.setUserRole(request.getRole()); userInfo.setCreateTime(java.time.LocalDateTime.now()); userInfo.setUpdateTime(java.time.LocalDateTime.now()); userRepository.save(userInfo); log.info("用户注册成功: {}", request.getUsername()); Map<String, Object> data = new HashMap<>(); data.put("userId", userInfo.getUserId()); data.put("username", userInfo.getUsername()); data.put("role", userInfo.getUserRole()); return RestResult.success(data); } catch (Exception e) { log.error("用户注册失败", e); return RestResult.fail("999999", "服务器内部错误"); } } else { return RestResult.fail("000002", "用户名或密码不能为空"); } } @Override public RestResult<?> login(LoginRequest request) { log.info("开始用户登录验证: {}", request.getUsername()); if (!StringUtils.hasText(request.getUsername()) || !StringUtils.hasText(request.getPassword())) { return RestResult.fail("000002", "用户名或密码不能为空"); } try { UserInfo user = userRepository.findByUsername(request.getUsername()) .orElse(null); if (user == null || !validatePassword(request.getPassword(), user.getPassword())) { return RestResult.fail("000001", "用户名或密码错误"); } // 生成JWT Token String token = generateToken(user); Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("username", user.getUsername()); data.put("role", user.getUserRole()); return RestResult.success(data); } catch (Exception e) { log.error("用户登录异常", e); return RestResult.fail("999999", "服务器内部错误"); } } @Override public RestResult<?> validateToken(String token) { log.info("开始校验Token有效性"); try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey.getBytes()) .build() .parseClaimsJws(token) .getBody(); String username = claims.getSubject(); String roleStr = (String) claims.get("role"); UserRole role = UserRole.valueOf(roleStr.toUpperCase()); Map<String, Object> data = new HashMap<>(); data.put("username", username); data.put("role", role); return RestResult.success(data); } catch (Exception e) { log.warn("Token无效或已过期", e); return RestResult.fail("000001", "Token无效或已过期"); } } @Override public RestResult<?> assignRole(RoleAssignRequest request) { log.info("开始为用户分配角色: userId={}, role={}", request.getUserId(), request.getRole()); UserInfo user = userRepository.findById(request.getUserId()).orElse(null); if (user == null) { return RestResult.fail("000001", "用户不存在"); } try { user.setUserRole(request.getRole()); user.setUpdateTime(java.time.LocalDateTime.now()); userRepository.save(user); Map<String, Object> data = new HashMap<>(); data.put("userId", user.getUserId()); data.put("username", user.getUsername()); data.put("role", user.getUserRole()); return RestResult.success(data); } catch (Exception e) { log.error("更新用户角色失败", e); return RestResult.fail("999999", "服务器内部错误"); } } /** * 密码加密模拟方法 * * @param rawPassword 明文密码 * @return 加密后的密码 */ private String encryptPassword(String rawPassword) { // 实际项目中应该使用BCrypt等强哈希算法进行加密 return rawPassword; // 此处仅为示例,真实场景需加强加密处理 } /** * 密码验证模拟方法 * * @param rawPassword 明文密码 * @param encodedPassword 已加密密码 * @return 是否匹配 */ private boolean validatePassword(String rawPassword, String encodedPassword) { // 实际项目中应该使用BCrypt等算法进行比对 return rawPassword.equals(encodedPassword); // 此处仅为示例,真实场景需加强加密处理 } /** * 生成JWT Token * * @param user 用户信息 * @return Token字符串 */ private String generateToken(UserInfo user) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + 86400000); // 设置一天有效期 Key key = new SecretKeySpec(secretKey.getBytes(), SignatureAlgorithm.HS512.getJcaName()); return Jwts.builder() .setSubject(user.getUsername()) .claim("role", user.getUserRole().toString()) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(key, SignatureAlgorithm.HS512) .compact(); }}一键获取完整项目代码plain2. 应用价值与延伸该学生成绩管理系统的落地,不仅解决了传统管理模式的痛点,更具备三大核心价值:效率提升:将教师成绩录入、管理员数据汇总的时间成本降低 70%,释放教学管理人力投入核心教学工作;数据安全:基于 RBAC 权限模型与操作日志追溯,实现 “谁操作、谁负责”,成绩修改记录永久留存,杜绝信息泄露与数据篡改风险;可扩展性:系统预留 “对接教务系统”“接入人脸识别签到数据” 等接口,未来可扩展 “成绩与考勤关联分析”“个性化学习推荐” 等功能,适配高校教学管理的长期发展需求。此外,借助飞算 JavaAI 的开发模式,相比传统手动编码,整体开发周期从原本的 30 天缩短至 7 天,且生成的代码符合行业规范,后期维护成本降低 50%,为高校毕业设计、中小型教学管理系统开发提供了高效、低成本的实现路径。原文链接:https://blog.csdn.net/2501_91822091/article/details/151220688
-
ava在AI时代的崛起:从传统机器学习到AIGC的全栈解决方案在人工智能浪潮席卷全球的今天,Python凭借其丰富的AI生态系统成为了机器学习和深度学习的首选语言。然而,作为企业级应用开发的王者,Java在AI领域的表现同样不容小觑。本文将深入探讨Java在AI生态中的定位、技术栈以及在AIGC时代的机遇与挑战。一、Java AI生态概览:多样化的技术选择Java在AI领域的技术栈可以用"百花齐放"来形容,从传统机器学习到现代深度学习,从自然语言处理到计算机视觉,Java都有相应的解决方案。1.1 深度学习框架:接轨主流AI技术Deep Java Library (DJL) - 统一的深度学习接口DJL是Amazon开源的Java深度学习库,其最大的优势在于提供了统一的API来操作不同的深度学习后端。// DJL示例:使用预训练模型进行图像分类import ai.djl.Application;import ai.djl.Model;import ai.djl.inference.Predictor;import ai.djl.modality.Classifications;import ai.djl.modality.cv.Image;import ai.djl.modality.cv.ImageFactory;import ai.djl.repository.zoo.Criteria;import ai.djl.repository.zoo.ModelZoo;import ai.djl.repository.zoo.ZooModel;public class ImageClassificationExample { public static void main(String[] args) throws Exception { // 加载预训练的ResNet模型 Criteria<Image, Classifications> criteria = Criteria.builder() .optApplication(Application.CV.IMAGE_CLASSIFICATION) .setTypes(Image.class, Classifications.class) .optFilter("layer", "50") .optEngine("PyTorch") .build(); try (ZooModel<Image, Classifications> model = ModelZoo.loadModel(criteria)) { try (Predictor<Image, Classifications> predictor = model.newPredictor()) { Image image = ImageFactory.getInstance().fromUrl("https://example.com/cat.jpg"); Classifications classifications = predictor.predict(image); System.out.println("预测结果:"); classifications.items().forEach(classification -> System.out.printf("%s: %.2f%%\n", classification.getClassName(), classification.getProbability() * 100)); } } }}运行项目并下载源码java运行Deeplearning4j - 企业级深度学习解决方案DL4J专为Java生态系统设计,特别适合需要与现有Java应用集成的场景。// DL4J示例:构建简单的神经网络import org.deeplearning4j.nn.conf.MultiLayerConfiguration;import org.deeplearning4j.nn.conf.NeuralNetConfiguration;import org.deeplearning4j.nn.conf.layers.DenseLayer;import org.deeplearning4j.nn.conf.layers.OutputLayer;import org.deeplearning4j.nn.multilayer.MultiLayerNetwork;import org.deeplearning4j.nn.weights.WeightInit;import org.nd4j.linalg.activations.Activation;import org.nd4j.linalg.lossfunctions.LossFunctions;public class SimpleNeuralNetwork { public static void main(String[] args) { // 构建神经网络配置 MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder() .seed(123) .weightInit(WeightInit.XAVIER) .updater(new org.nd4j.linalg.learning.config.Adam(0.001)) .list() .layer(0, new DenseLayer.Builder() .nIn(784) // 输入层大小 .nOut(128) // 隐藏层大小 .activation(Activation.RELU) .build()) .layer(1, new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD) .nIn(128) .nOut(10) // 输出类别数 .activation(Activation.SOFTMAX) .build()) .build(); MultiLayerNetwork model = new MultiLayerNetwork(conf); model.init(); System.out.println("神经网络构建完成,参数数量: " + model.numParams()); }}运行项目并下载源码java运行原文链接:https://blog.csdn.net/weixin_44976692/article/details/149835949
-
多张图合成JPG是Java图像处理中一个常见的需求场景。无论是制作拼接式海报、组合式证件照,还是生成带有多种元素的创意图片,都需要将多张图片按照特定的规则和布局合并成一张JPG格式的图像。然而,在实际操作过程中,开发人员常常会遇到一些棘手的问题,其中红色前景异常和多行列自适应适配难题尤为突出。红色前景问题的出现,往往会让合成后的图像产生不自然的视觉效果,原本应该和谐融合的画面被突兀的红色所破坏,这不仅影响了图像的美观度,还可能导致信息传达的不准确。例如,在合成一张包含人物和风景的图片时,如果人物部分出现红色前景,会让人误以为是图像的错误标注或者质量缺陷,极大地降低了合成图像的专业性和可信度。 解决这些问题不仅是提升Java图像处理技术质量的关键一步,更是满足市场需求、提升用户满意度的必然要求。通过深入研究和探索有效的解决方案,我们能够使合成后的JPG图像在视觉效果上更加完美、自然,同时确保其在各种展示环境下的兼容性和适应性,为用户提供更加优质、流畅的视觉体验,从而推动Java图像处理技术在更多领域的广泛应用和持续发展。本文即讲解在Java中解决多张JPG合成时出现红色前景以及图片指定列数的自适应适配解决方案。 一、追本溯源 在之前的博客中,链接:基于Java的自助多张图片合成拼接实战。在博客中讲解了如何利用Java程序来自动合成图片。当时博客使用的示例图片来源是png格式的,也没有什么问题。后面遇到一个场景,需求使用的场景是图片来源格式是jpg,根据jpg来合成时遇到了合成的图片的前置颜色是红色的情况。本文就来解决这种问题以及指定列数自适应的问题。 1、回到最开始 实验的开始,首先来还原现场,首先来准备6张格式是jpg的图片,如下图所示: 图片合成时,在创建BufferedImage时,首先使用的是合并代码如下: /** * -合并图片 * @param images 压缩后的图片数组 * @param imagesPerRow 每行的图片数量 * @return 合并后的图片 */public static BufferedImage mergeImages(BufferedImage[] images, int imagesPerRow) { int totalWidth = images[0].getWidth() * imagesPerRow; int totalHeight = (int) Math.ceil((double) images.length / imagesPerRow) * images[0].getHeight(); BufferedImage mergedImage = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = mergedImage.createGraphics(); g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, totalWidth, totalHeight); int x = 0; int y = 0; for (int i = 0; i < images.length; i++) { g2d.drawImage(images[i], x, y, null); x += images[i].getWidth(); if ((i + 1) % imagesPerRow == 0) { x = 0; y += images[i].getHeight(); } } g2d.dispose(); return mergedImage;}一键获取完整项目代码java 2、合成JPG的异常 使用多张图片合成jpg的关键代码如下: public static void mergeOriginalJpg() {String common = "D:/imagemerge/original/jpg/"; // 图片路径列表String[] imagePaths = {common + "chongqing.jpg", common + "guangdong.jpg", common + "hunan.jpg", common + "jiangsu.jpg", common + "liaoning.jpg", common + "xinjiang.jpg"}; // 输出图片路径 //String outputImagePath = "D:/imagemerge/new/merged_image_jpg_rgb.jpg";String outputImagePath = "D:/imagemerge/new/merged_image_jpg_argb.jpg"; imageMerge("jpg",imagePaths,outputImagePath);}一键获取完整项目代码java 在IDE中运行以上程序,在目标磁盘可以看到以下的图片结果: 这就是我们前文说过的红色背景的问题,就像蒙上了一层红色的灰蒙蒙色彩一样,质量不清晰。 二、解决问题 遇到了问题,就来解决问题。本节将重点讲述如何来解决这个问题。主要的解决办法是修改创建图源时的imageTpye。本章节首先介绍什么事imageType,其它介绍两个常用的imageType,比如BufferedImage.TYPE_INT_ARGB和BufferedImage.TYPE_INT_RGB,最后讲解如何进行问题的修复以及指定列数后的自适应设置。 1、关于ImageTypeBufferedImage mergedImage = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_ARGB);一键获取完整项目代码java 以上是在Java中创建BufferedImage对象的构造方法,构造参数是三个信息,宽度、高度和图片类型。而这里的图片类型就是决定了我们的图片生成结果的主要因素。 2、TYPE_INT_RGB和TYPE_INT_ARGB 为了弄清楚生成图片时的参数差异,这里我们主要介绍BufferedImage.TYPE_INT_ARGB和BufferedImage.TYPE_INT_RGB。当然,在BufferedImage中还有其它的参数,但是这里仅介绍这两个,其它的参数类型感兴趣的可以自己去查资料了解。BufferedImage 类型的不同会影响图像的存储方式、颜色模式、透明度支持以及最终的显示效果。以下是 BufferedImage.TYPE_INT_ARGB 和其他常见类型(如 BufferedImage.TYPE_INT_RGB)的主要区别及其对结果的影响: 1. BufferedImage.TYPE_INT_ARGB 定义:表示一个图像,具有 8 位 RGBA 颜色分量,其中 A(Alpha)表示透明度,R(Red)、G(Green)、B(Blue)表示颜色分量。 特点: 支持透明度(Alpha 通道)。 每个像素占用 4 个字节(32 位),其中 8 位用于透明度,24 位用于颜色。 适用于需要透明效果的图像处理,例如合成带有透明背景的图片。 对结果的影响: 可以处理透明度,避免合成时背景变成黑色或白色。 在合成图片时,可以更好地保留原始图片的透明区域,避免颜色失真。 由于支持透明度,文件大小可能会比不支持透明度的类型稍大。 2. BufferedImage.TYPE_INT_RGB 定义:表示一个图像,具有 8 位 RGB 颜色分量,不包含透明度信息。 特点: 不支持透明度。 每个像素占用 3 个字节(24 位),分别用于 R、G、B 颜色分量。 适用于不需要透明效果的图像处理。 对结果的影响: 由于不支持透明度,合成时可能会导致背景颜色被填充为默认值(通常是黑色或白色),从而影响图片的视觉效果。 文件大小相对较小,因为没有透明度信息。 在处理透明图片时,可能会丢失透明区域的颜色信息,导致合成后的图片出现灰蒙蒙的效果。 通过以上的对比,相信大家应该有一个简单的认识,即我们出现问题的原因是什么?其实就是这个ImageType的设置问题导致了出现红色蒙版的现象。 3、问题修复 了解了大致的问题之后,我们就可以对症下药。既然是图片类型的设置不正确,那么我们就可以针对性的进行设置正确参数。这里我们将图片的类型从BufferedImage.TYPE_INT_ARGB改为:BufferedImage.TYPE_INT_RGB。然后再调用系统代码来实现合成图片,代码如下: BufferedImage mergedImage = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_RGB);一键获取完整项目代码java 使用上面的测试代码在IDE中运行之后,在对应的磁盘中发现生成的图片已经把红色的蒙版去掉了,恢复了本来的面目,如下图所示: 至此,解决图片合成JPG时出现红色蒙版的问题解决。 同时,经过上面的处理,将imagetype进行替代后,能同时处理png的图片,也能处理jpg的图片。 4、列数自适应的问题 在合成图片时,我们需要指定列数,比如每一行展示几张图片,根据需要,我们可以设置一个任意的数字,这里我们选择的设置是每行2张。在系统合成时,会自动根据这个张数来计算合成后的图片宽度。处理方法如下: /*** -加载并等比例压缩图片* @param imagePaths 图片路径数组* @param imagesPerRow 每行的图片数量* @return 压缩后的图片数组* @throws IOException*/public static BufferedImage[] loadAndResizeImages(String[] imagePaths, int imagesPerRow) throws IOException { BufferedImage[] images = new BufferedImage[imagePaths.length]; int maxWidth = 0; // 加载图片并计算最大宽度 for (int i = 0; i < imagePaths.length; i++) { BufferedImage image = ImageIO.read(new File(imagePaths[i])); maxWidth = Math.max(maxWidth, image.getWidth()); images[i] = image; } // 等比例压缩图片 int targetWidth = maxWidth / imagesPerRow; for (int i = 0; i < images.length; i++) { images[i] = resizeImage(images[i], targetWidth); } return images;}一键获取完整项目代码java 原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/146327974
-
一、智能教育虚拟学习环境的现状与挑战1.1 传统学习环境的局限性传统在线教育模式往往呈现出 “单向灌输” 的特征。以某知名在线课程平台为例,课程内容多以录播视频为主,学生仅能按固定节奏观看教学视频、完成标准化测试。据权威机构调研数据显示,在传统在线学习模式下,学生平均课程完成率不足 40%,超 65% 的学生反馈学习过程缺乏互动性与个性化引导 。这种模式难以满足不同学生的学习节奏与知识掌握程度,导致学习效率低下,学习积极性受挫。1.2 虚拟学习环境的发展瓶颈当前的虚拟学习环境虽已取得一定进展,但仍存在诸多亟待解决的问题。在技术层面,部分虚拟实验室的 3D 场景渲染效果粗糙,交互逻辑简单,学生在操作过程中难以获得真实的实验体验;在数据层面,学习行为数据的采集碎片化,无法形成完整的学生学习画像,导致教师难以针对性地调整教学策略。以下用表格形式直观展示常见问题:问题类型 具体表现 对学习的影响场景体验问题 虚拟场景建模精度低、交互卡顿 学习沉浸感不足,注意力易分散数据采集问题 仅记录答题结果,缺乏过程性数据 无法精准分析学生知识薄弱点个性化不足 统一教学内容与进度 基础好的学生 “吃不饱”,基础弱的学生 “跟不上”二、Java 大数据构建智能教育虚拟学习环境的核心技术2.1 多源数据采集与整合在智能教育虚拟学习环境中,Java 凭借其强大的网络编程能力,可实现多维度数据采集。通过 WebSocket 协议实时捕获学生在虚拟环境中的操作行为,如鼠标点击位置、拖拽动作、语音交流内容等;结合 Java Servlet 技术,可从服务器端获取课程浏览记录、测试成绩等数据。以下是完整的 WebSocket 数据采集 Java 代码示例,并添加详细注释:import javax.websocket.*;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.util.concurrent.CopyOnWriteArraySet;// 定义WebSocket服务端点,指定访问路径为/learning-data@ServerEndpoint("/learning-data") public class LearningDataCollector { // 存储所有连接的会话,使用线程安全的CopyOnWriteArraySet private static CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<>(); // 当有新的WebSocket连接建立时触发此方法 @OnOpen public void onOpen(Session session) { sessions.add(session); System.out.println("新连接已建立,当前连接数:" + sessions.size()); } // 当接收到客户端发送的消息时触发此方法 @OnMessage public void onMessage(String message, Session session) throws IOException { // 此处可对接收到的消息进行解析,例如JSON格式数据解析 // 简单示例:假设message为JSON字符串,包含学习行为数据 System.out.println("收到学习行为数据:" + message); // 可以将数据进一步处理后存储到数据库或消息队列中 } // 当WebSocket连接关闭时触发此方法 @OnClose public void onClose(Session session) { sessions.remove(session); System.out.println("连接已关闭,当前连接数:" + sessions.size()); } // 当连接过程中发生错误时触发此方法 @OnError public void onError(Session session, Throwable error) { System.out.println("连接发生错误:" + error.getMessage()); error.printStackTrace(); }}一键获取完整项目代码java采集到的数据需通过 Hive 数据仓库进行结构化存储与整合。创建如下 Hive 表结构,用于存储学生学习行为数据:-- 创建学习行为表CREATE TABLE learning_behavior ( student_id string COMMENT '学生ID', action_type string COMMENT '行为类型,如点击、拖拽、答题等', action_time timestamp COMMENT '行为发生时间', content_id string COMMENT '操作的内容ID,如课程章节ID、题目ID', session_id string COMMENT '会话ID')PARTITIONED BY (course_id string COMMENT '课程ID')STORED AS ORC;一键获取完整项目代码sql2.2 大数据分析与处理框架Apache Spark 和 Flink 作为 Java 大数据处理的核心框架,在智能教育场景中发挥着关键作用。Spark 用于离线分析历史学习数据,挖掘学生学习模式。例如,使用 Spark SQL 分析学生在不同课程模块的学习时长分布:-- 使用Spark SQL分析各课程模块平均学习时长SELECT course_module_id, AVG(study_duration) AS avg_study_time FROM learning_data GROUP BY course_module_id;一键获取完整项目代码sqlFlink 则专注于实时流处理,可即时分析学生的学习行为,实现动态预警。当检测到学生连续多次错误回答同一类型题目时,立即向教师端发送提醒。以下是基于 Flink 的实时异常行为检测 Java 代码:import org.apache.flink.api.common.functions.FilterFunction;import org.apache.flink.api.java.tuple.Tuple2;import org.apache.flink.streaming.api.datastream.DataStream;import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;public class LearningAnomalyDetection { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 模拟从Kafka或其他数据源读取学生答题数据,格式为(学生ID, 答题结果) DataStream<Tuple2<String, Boolean>> answerStream = env.fromElements( Tuple2.of("student001", true), Tuple2.of("student001", false), Tuple2.of("student001", false), Tuple2.of("student001", false) ); // 过滤出连续答错3次以上的学生数据 DataStream<Tuple2<String, Boolean>> anomalyStream = answerStream.keyBy(t -> t.f0) .countWindow(3) .filter(new FilterFunction<Tuple2<String, Boolean>>() { private static final long serialVersionUID = 1L; @Override public boolean filter(Tuple2<String, Boolean> value) throws Exception { int wrongCount = 0; for (Tuple2<String, Boolean> v : value.getField(1)) { if (!v.f1) { wrongCount++; } } return wrongCount >= 3; } }); anomalyStream.print("异常答题行为:"); env.execute("学习异常行为检测"); }}一键获取完整项目代码java三、Java 大数据优化用户体验的实践路径3.1 个性化学习路径推荐基于学生的历史学习数据,使用协同过滤算法构建个性化学习路径推荐系统。通过计算学生之间的学习相似度,为目标学生推荐相似学生的学习内容与进度。以下是简化版的协同过滤算法 Java 实现代码:import java.util.ArrayList;import java.util.List;public class CollaborativeFiltering { // 模拟学生学习记录矩阵,行代表学生,列代表课程,值为学习进度 private static int[][] studyRecords = { {80, 60, 40}, {90, 70, 50}, {30, 20, 10} }; // 计算两个学生的余弦相似度 public static double cosineSimilarity(int[] user1, int[] user2) { double dotProduct = 0; double normUser1 = 0; double normUser2 = 0; for (int i = 0; i < user1.length; i++) { dotProduct += user1[i] * user2[i]; normUser1 += Math.pow(user1[i], 2); normUser2 += Math.pow(user2[i], 2); } return dotProduct / (Math.sqrt(normUser1) * Math.sqrt(normUser2)); } // 为目标学生推荐课程 public static List<Integer> recommendCourses(int targetUserIndex) { List<Integer> recommendedCourses = new ArrayList<>(); double maxSimilarity = -1; int mostSimilarUserIndex = -1; for (int i = 0; i < studyRecords.length; i++) { if (i != targetUserIndex) { double similarity = cosineSimilarity(studyRecords[targetUserIndex], studyRecords[i]); if (similarity > maxSimilarity) { maxSimilarity = similarity; mostSimilarUserIndex = i; } } } if (mostSimilarUserIndex != -1) { for (int i = 0; i < studyRecords[mostSimilarUserIndex].length; i++) { if (studyRecords[targetUserIndex][i] < studyRecords[mostSimilarUserIndex][i]) { recommendedCourses.add(i); } } } return recommendedCourses; } public static void main(String[] args) { int targetUser = 0; List<Integer> recommended = recommendCourses(targetUser); System.out.println("为学生" + targetUser + "推荐的课程索引:" + recommended); }}一键获取完整项目代码java3.2 实时学习反馈与互动增强利用 Java 开发实时互动模块,结合 WebSocket 实现师生即时沟通。当学生在虚拟实验过程中遇到问题时,可通过内置聊天窗口发送求助信息,教师端实时接收并给予指导。同时,系统根据学生的学习行为数据,动态调整虚拟场景中的学习任务难度。例如,当检测到学生对某知识点掌握较好时,自动推送更具挑战性的拓展任务。四、经典案例:某在线教育平台的智能转型实践某头部在线教育平台在引入 Java 大数据技术后,对虚拟学习环境进行了全面升级。通过部署上述多源数据采集系统,平台日均采集学习行为数据超 5TB,涵盖 100 万 + 学生的操作记录。基于 Spark 和 Flink 构建的数据分析平台,实现了以下成果:个性化推荐成效显著:学生平均课程完成率从 38% 提升至 65%,个性化推荐的课程点击率比传统推荐方式高 40%。实时互动增强粘性:引入实时聊天与智能反馈功能后,学生日均在线时长增加 30 分钟,用户留存率提高 25%。教学质量精准提升:教师通过数据分析平台,可快速定位学生的知识薄弱点,针对性调整教学内容,课程满意度提升至 92%。五、未来展望与技术挑战尽管 Java 大数据在智能教育虚拟学习环境中已取得显著成果,但仍面临诸多挑战。随着元宇宙技术的发展,对虚拟场景的实时渲染与数据处理提出更高要求;同时,学生数据的隐私保护与合规使用也是亟待解决的问题。未来,Java 大数据技术将与人工智能、虚拟现实等前沿技术深度融合,进一步推动智能教育的创新发展。原文链接:https://blog.csdn.net/atgfg/article/details/154616254
-
一、金融风险压力测试的现状与挑战1.1 传统压力测试的局限性传统的金融风险压力测试,就像戴着 “老花镜” 看世界,难以看清复杂多变的风险全貌。以 2008 年全球金融危机为例,众多国际金融机构采用基于历史数据的简单模型进行压力测试,仅考虑少数几个风险因子,对次级贷款市场的潜在风险缺乏足够的预判。某知名投行在危机前,其压力测试模型预估的损失与实际亏损相差数十倍,最终因资金链断裂而轰然倒塌。据统计,当时全球约 70% 的金融机构在压力测试中存在风险低估的情况,传统方法的局限性暴露无遗。这些传统模型通常基于静态假设,无法适应金融市场快速变化的节奏。数据更新滞后、风险因子覆盖不全、模型灵活性差等问题,使得压力测试难以准确评估金融机构在极端情况下的风险承受能力。在金融创新不断推进的今天,传统压力测试已经成为制约金融机构稳健发展的瓶颈。1.2 新时代金融风险的复杂性随着金融科技的蓬勃发展,金融市场的风险格局发生了巨大变化。数字货币、量化交易、供应链金融等新兴业务不断涌现,带来了诸如技术风险、算法风险、智能合约风险等新型风险。2022 年,某加密货币交易平台因智能合约漏洞,被黑客攻击,导致数亿美元的资产被盗取,引发市场恐慌。同时,全球金融市场的关联性日益增强,地缘政治冲突、气候变化、网络安全事件等非传统因素,也成为影响金融稳定的重要变量。这些风险相互交织、相互传导,形成了复杂的风险网络。传统的压力测试方法,由于其单一的数据来源和简单的分析模型,已经无法全面、准确地评估这些复杂风险,亟需引入更先进的技术手段。 二、Java 大数据机器学习模型技术基石2.1 多源数据采集与整合在金融数据的浩瀚海洋中,Java 凭借其强大的网络编程能力和丰富的开源生态,成为了高效采集数据的 “超级战舰”。通过 OkHttp 库,我们可以轻松地从各类专业数据平台获取实时行情数据。以下是一个获取苹果公司(AAPL)股票历史数据的完整代码示例,代码中详细注释了每一步的操作目的和实现逻辑:import okhttp3.*;import java.io.IOException;public class StockDataCollector { // Alpha Vantage API密钥,使用时需替换为实际申请的有效密钥 private static final String API_KEY = "YOUR_API_KEY"; // 创建OkHttp客户端实例,用于发起HTTP请求,单例模式提高性能 private static final OkHttpClient client = new OkHttpClient(); /** * 根据股票代码获取历史数据 * @param symbol 股票代码,例如"AAPL"代表苹果公司 * @return 包含股票历史数据的JSON格式字符串 * @throws IOException 网络请求失败或数据读取异常时抛出 */ public static String getStockData(String symbol) throws IOException { // 拼接API请求URL,指定获取每日调整后的股票数据 String url = "https://www.alpha-vantage.co/query?function=TIME_SERIES_DAILY_ADJUSTED&symbol=" + symbol + "&apikey=" + API_KEY; // 构建HTTP GET请求对象 Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { // 判断请求是否成功(状态码为200表示成功) if (response.isSuccessful()) { // 读取并返回响应体中的数据 return response.body().string(); } else { // 请求失败时抛出异常,包含错误信息 throw new IOException("Unexpected code " + response); } } } public static void main(String[] args) { try { String data = getStockData("AAPL"); System.out.println("苹果公司股票数据: " + data); } catch (IOException e) { e.printStackTrace(); } }}一键获取完整项目代码java采集到的数据往往是 “raw material”,需要进行精细加工。利用 Java 8 Stream API 和 Jackson 库,我们可以对数据进行清洗、解析和结构化处理,去除无效数据,统一数据格式,为后续的模型训练提供高质量的 “原材料”。以下是数据处理的代码实现:import com.fasterxml.jackson.databind.ObjectMapper;import java.io.IOException;import java.util.List;import java.util.stream.Collectors;public class StockDataProcessor { /** * 处理原始股票数据,将其转换为结构化的股票记录列表,并剔除异常数据 * @param rawData 从数据平台获取的原始JSON格式股票数据 * @return 包含有效股票记录的列表 * @throws IOException JSON解析过程中出现异常时抛出 */ public static List<StockRecord> processData(String rawData) throws IOException { ObjectMapper mapper = new ObjectMapper(); // 将JSON数据反序列化为自定义的数据包装类 StockDataWrapper wrapper = mapper.readValue(rawData, StockDataWrapper.class); return wrapper.getTimeSeries().entrySet().stream() .map(entry -> { String date = entry.getKey(); StockRecord record = entry.getValue(); record.setDate(date); return record; }) // 过滤掉成交量为0或无效的记录 .filter(record -> Double.parseDouble(record.getVolume()) > 0) .collect(Collectors.toList()); } // 自定义数据类:用于包装API返回的时间序列数据 static class StockDataWrapper { private java.util.Map<String, StockRecord> timeSeries; public java.util.Map<String, StockRecord> getTimeSeries() { return timeSeries; } public void setTimeSeries(java.util.Map<String, StockRecord> timeSeries) { this.timeSeries = timeSeries; } } // 自定义数据类:代表单条股票记录,包含各项关键数据字段 static class StockRecord { private String open; private String high; private String low; private String close; private String adjustedClose; private String volume; private String dividendAmount; private String splitCoefficient; private String date; // 省略各字段的getter和setter方法 }}一键获取完整项目代码java2.2 机器学习模型构建与优化在金融风险压力测试的 “武器库” 中,随机森林(Random Forest)和长短期记忆网络(LSTM)是两款极具威力的 “利器”。随机森林擅长处理结构化数据,能够有效应对金融领域中的高维特征。下面是基于 Apache Spark MLlib 构建信用风险评估模型的完整代码,从数据读取、特征工程到模型训练、评估,每一个环节都清晰呈现:import org.apache.spark.ml.classification.RandomForestClassifier;import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator;import org.apache.spark.ml.feature.VectorAssembler;import org.apache.spark.sql.Dataset;import org.apache.spark.sql.Row;import org.apache.spark.sql.SparkSession;public class CreditRiskModel { public static void main(String[] args) { // 创建SparkSession实例,设置应用名称和运行模式 SparkSession spark = SparkSession.builder() .appName("CreditRiskModel") .master("local[*]") .getOrCreate(); // 读取信用数据CSV文件,包含收入、负债、历史违约记录等特征以及风险标签 Dataset<Row> data = spark.read().csv("credit_data.csv") .toDF("income", "liability", "default_history", "risk_label"); // 特征工程:将多个数值特征合并为一个特征向量,便于模型处理 VectorAssembler assembler = new VectorAssembler() .setInputCols(new String[]{"income", "liability", "default_history"}) .setOutputCol("features"); Dataset<Row> assembledData = assembler.transform(data); // 将数据集划分为训练集(70%)和测试集(30%) Dataset<Row>[] splits = assembledData.randomSplit(new double[]{0.7, 0.3}); Dataset<Row> trainingData = splits[0]; Dataset<Row> testData = splits[1]; // 构建随机森林分类模型,指定标签列和特征列 RandomForestClassifier rf = new RandomForestClassifier() .setLabelCol("risk_label") .setFeaturesCol("features"); // 使用训练数据训练模型 org.apache.spark.ml.classification.RandomForestClassificationModel model = rf.fit(trainingData); // 使用训练好的模型对测试数据进行预测 Dataset<Row> predictions = model.transform(testData); // 展示预测结果,包括预测标签、真实标签和预测概率 predictions.select("prediction", "risk_label", "probability").show(); // 模型评估:使用多分类评估器计算模型的准确率 MulticlassClassificationEvaluator evaluator = new MulticlassClassificationEvaluator() .setLabelCol("risk_label") .setPredictionCol("prediction"); double accuracy = evaluator.evaluate(predictions); System.out.println("模型准确率: " + accuracy); // 关闭SparkSession,释放资源 spark.stop(); }}一键获取完整项目代码java对于具有时序特性的金融市场数据,LSTM 模型能够捕捉数据中的长期依赖关系,在预测市场趋势等方面表现出色。基于 Deeplearning4j 框架构建的汇率波动预测模型如下,详细注释了模型构建和训练的关键步骤:import org.deeplearning4j.nn.conf.MultiLayerConfiguration;import org.deeplearning4j.nn.conf.NeuralNetConfiguration;import org.deeplearning4j.nn.conf.layers.LSTM;import org.deeplearning4j.nn.conf.layers.OutputLayer;import org.deeplearning4j.nn.multilayer.MultiLayerNetwork;import org.nd4j.linalg.activations.Activation;import org.nd4j.linalg.api.ndarray.INDArray;import org.nd4j.linalg.dataset.DataSet;import org.nd4j.linalg.dataset.api.iterator.DataSetIterator;import org.nd4j.linalg.lossfunctions.LossFunctions;public class ExchangeRatePrediction { /** * 构建LSTM神经网络模型 * @return 初始化后的多层神经网络模型 */ public static MultiLayerNetwork buildLSTMModel() { MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder() .seed(12345) .weightInit(org.deeplearning4j.nn.weights.WeightInit.XAVIER) .list() .layer(new LSTM.Builder() .nIn(10) // 假设输入10个特征,如汇率、利率等相关指标 .nOut(50) .activation(Activation.TANH) .build()) .layer(new LSTM.Builder() .nIn(50) .nOut(50) .activation(Activation.TANH) .build()) .layer(new OutputLayer.Builder() .nOut(1) // 输出单个预测值,即汇率预测结果 .activation(Activation.IDENTITY) .lossFunction(LossFunctions.LossFunction.MSE) .build()) .build(); return new MultiLayerNetwork(conf); } public static void main(String[] args) { MultiLayerNetwork model = buildLSTMModel(); model.init(); // 假设从CSV文件读取汇率历史数据,这里需要自定义数据集迭代器 DataSetIterator dataIterator = new ExchangeRateDataSetIterator("exchange_rate_data.csv", 32); while (dataIterator.hasNext()) { DataSet batch = dataIterator.next(); INDArray features = batch.getFeatureMatrix(); INDArray labels = batch.getLabels(); // 使用批次数据训练模型 model.fit(features, labels); } // 对新数据进行预测 INDArray newData = dataIterator.next().getFeatureMatrix(); INDArray prediction = model.output(newData); System.out.println("汇率预测结果: " + prediction); }}一键获取完整项目代码java2.3 模型对比与挑战分析在实际应用中,不同的机器学习模型各有优劣。RandomForest 模型具有良好的可解释性,通过计算特征重要性,能够直观地展示各个因素对风险评估结果的影响程度,这对于金融机构向监管部门解释风险评估过程非常重要。然而,XGBoost 模型在训练速度和预测精度上更具优势。某城市商业银行在信用卡违约预测项目中对比发现,XGBoost 模型的 AUC 值比 RandomForest 模型高 0.08,训练时间缩短了 30%。因此,在选择模型时,需要根据具体的业务需求和数据特点进行综合考量,甚至可以采用模型融合的策略,以充分发挥不同模型的优势。同时,机器学习模型在金融领域的应用也面临诸多挑战。一方面,模型的可解释性问题一直是制约其广泛应用的关键因素。例如,LSTM 等深度学习模型就像一个 “黑匣子”,难以解释其预测结果的依据,这在金融监管日益严格的环境下,可能导致模型无法通过审核。另一方面,数据隐私和安全问题也不容忽视。在跨机构数据共享和联合建模过程中,如何确保数据不被泄露,如何满足《个人信息保护法》《数据安全法》等法律法规的要求,是必须解决的难题。目前,联邦学习、安全多方计算等技术为这些问题提供了新的解决方案,但在实际应用中仍需要不断探索和完善。2.4 前沿技术融合实践联邦学习协同风控:联邦学习技术打破了数据孤岛,让金融机构在不泄露用户隐私的前提下实现数据共享和协同建模。基于 Java 开发的 FATE(Federated AI Technology Enabler)框架,已经在多个金融场景中得到成功应用。某省的银行联盟通过 FATE 框架,联合当地的电商平台和征信机构,共享小微企业的交易数据、信用数据等,构建了联合风控模型。在不传输原始数据的情况下,各方仅交换加密的模型参数,最终训练出的模型将小微企业贷款违约预测准确率从 75% 提升到了 88%,同时有效降低了信贷风险。强化学习动态决策:强化学习通过让模型与环境进行交互,不断学习最优策略,为金融风险决策提供了新的思路。某国际投资机构将强化学习与 LSTM 相结合,应用于外汇投资组合管理。模型通过不断模拟市场环境的变化,实时调整投资组合的权重。在 2023 年全球外汇市场剧烈波动期间,该模型实现了收益波动率降低 22%,投资回报率提升 15% 的优异成绩。这种动态决策机制,使得金融机构能够更好地应对市场的不确定性,提升风险应对能力。三、Java 大数据机器学习模型在金融风险压力测试中的创新应用3.1 信用风险动态评估Java 大数据机器学习模型能够整合企业的财务报表、税务数据、司法诉讼记录、社交媒体数据等多维度信息,构建动态的信用风险评估体系。某股份制银行引入该技术后,对小微企业的信用评估实现了从 “静态评估” 到 “动态监测” 的转变。系统每天自动更新企业数据,当监测到企业出现股权质押比例过高、高管变更频繁、涉诉记录增加等风险信号时,会立即触发预警,并重新评估企业的信用等级。通过该系统,银行将小微企业贷款的不良率从 5% 降低到了 3.2%,同时提高了信贷审批效率,平均审批时间从 5 个工作日缩短到了 1 个工作日。3.2 市场风险极端场景模拟利用蒙特卡洛模拟与机器学习相结合的方法,可以生成海量的极端市场场景,对金融机构的资产组合进行压力测试。某大型券商构建的市场风险压力测试平台,基于 Java 大数据技术,每天能够生成 10 万 + 种极端市场情景,涵盖股票市场暴跌、汇率大幅波动、利率突然调整等情况。在 2023 年美联储加息预期强烈的背景下,该平台提前模拟了多种加息场景对券商自营业务的影响。通过对历史数据的学习和分析,模型不仅能够模拟出市场价格的波动情况,还能预测不同资产之间的相关性变化,帮助券商提前调整资产配置,减少潜在损失。平台采用 Java 多线程技术加速模拟过程,通过分布式计算框架将任务分配到多个节点并行处理。以下是简化的蒙特卡洛模拟代码示例,用于估算投资组合在极端市场下的风险价值(VaR):import java.util.Random;public class MonteCarloVaR { // 投资组合初始价值 private static final double portfolioValue = 1000000; // 模拟次数 private static final int numSimulations = 10000; // 置信水平 private static final double confidenceLevel = 0.95; public static double calculateVaR(double[] historicalReturns) { double[] simulationReturns = new double[numSimulations]; Random random = new Random(); for (int i = 0; i < numSimulations; i++) { // 从历史收益率中随机抽样模拟未来收益率 int randomIndex = random.nextInt(historicalReturns.length); simulationReturns[i] = portfolioValue * (1 + historicalReturns[randomIndex]); } // 对模拟结果排序 java.util.Arrays.sort(simulationReturns); int index = (int) ((1 - confidenceLevel) * numSimulations); return portfolioValue - simulationReturns[index]; } public static void main(String[] args) { // 假设的历史收益率数据,实际应用中应从数据平台获取 double[] historicalReturns = {0.01, -0.02, 0.03, -0.015, 0.005}; double var = calculateVaR(historicalReturns); System.out.println("在" + (confidenceLevel * 100) + "%置信水平下的VaR值为: " + var); }}一键获取完整项目代码java3.3 操作风险智能预警利用自然语言处理(NLP)技术分析银行内部日志、客服记录、交易备注等非结构化数据,可识别操作风险信号。某支付机构通过 Java 开发的智能风控系统,实时监测交易备注中的敏感词(如 “测试”“紧急”)、异常操作指令序列,成功拦截 98% 的钓鱼攻击,将操作风险损失降低 40%。系统采用 BERT 预训练模型对文本进行语义理解,结合规则引擎实现风险的快速响应。以下是基于 Java 和 HanLP(汉语自然语言处理包)的敏感词检测示例代码:import com.hankcs.hanlp.HanLP;import com.hankcs.hanlp.dictionary.py.Pinyin;import com.hankcs.hanlp.seg.common.Term;import java.util.List;public class SensitiveWordDetector { private static final String[] sensitiveWords = {"测试", "钓鱼", "非法"}; public static boolean containsSensitiveWord(String text) { List<Term> termList = HanLP.segment(text); for (Term term : termList) { String word = term.word; for (String sensitive : sensitiveWords) { if (word.equals(sensitive) || Pinyin.toPinyin(word).contains(Pinyin.toPinyin(sensitive))) { return true; } } } return false; } public static void main(String[] args) { String transactionNote = "这是一笔测试交易"; if (containsSensitiveWord(transactionNote)) { System.out.println("检测到敏感词,交易存在风险!"); } else { System.out.println("未检测到敏感词,交易正常。"); } }}一键获取完整项目代码java四、标杆案例深度剖析4.1 案例一:花旗银行全球风险压力测试平台花旗银行基于 Java 大数据构建的全球风险压力测试平台,整合了 150 个国家的宏观经济数据、20 万 + 金融产品信息,是金融科技领域的标杆之作:技术架构:采用 Hadoop 分布式存储与 Spark Streaming 实时计算,日均处理 8TB 数据;通过 Kafka 实现数据的高吞吐传输,HBase 存储历史数据,构建起 PB 级数据仓库。模型能力:部署 200 + 个机器学习模型,覆盖信用、市场、流动性、操作四大风险领域;运用图计算技术分析金融机构间的关联网络,识别系统性风险传导路径。应用效果:将压力测试周期从 3 个月缩短至 72 小时,风险识别准确率提升 35%;在 2022 年欧洲能源危机中,提前预警能源衍生品敞口风险,避免潜在损失超 8 亿美元。经济效益:每年减少潜在损失超 12 亿美元,运营成本降低 25%。指标 传统方案 智能方案 提升幅度测试周期 3 个月 72 小时 ↓96.7%风险覆盖率 75% 98% ↑30.7%潜在损失减少(年) 8 亿美元 12 亿美元 +50%运营成本 - ↓25% -4.2 案例二:蚂蚁集团智能风控体系蚂蚁集团依托 Java 大数据与机器学习,打造了全球最大的金融智能风控系统:数据融合:整合 10 亿用户的消费行为、社交关系、地理位置等 200 + 维度数据,构建用户风险画像;通过知识图谱技术关联交易网络,识别团伙欺诈。模型创新:采用 GBDT+LR 混合模型,结合梯度提升树的非线性拟合能力与逻辑回归的可解释性,实现毫秒级交易风险识别;引入联邦学习技术,联合银行、电商等机构共享风控能力。应用成果:交易风险识别时间从 1000 毫秒缩短至 10 毫秒,每年拦截欺诈金额超 500 亿元;在 2023 年 “双 11” 购物节期间,保障单日交易笔数超 10 亿的情况下,资金损失率低于 0.01%。原文链接:https://blog.csdn.net/atgfg/article/details/154494420
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签