Java 之 final 详解
目录
一. 前言
二. final 的基础使用
2.1. 修饰类
2.2. 修饰方法
2.2.1. private 方法是隐式的 final
2.2.2. final 方法可以被重载
2.3. 修饰参数
2.4. 修饰变量
2.4.1. static final
2.4.2. blank final
2.4.3. 所有 final 修饰的字段都是编译期常量吗?
三. final 域重排序规则
3.1. final 域为基本类型
3.1.1. 写 final 域重排序规则
3.1.2. 读 final 域重排序规则
3.2. final 域为引用类型
3.2.1. 对 final 修饰的对象的成员域写操作
3.2.2. 对 final 修饰的对象的成员域读操作
3.3. final 重排序的总结
四. final 再深入理解
4.1. final 的实现原理
4.2. 为什么 final 引用不能从构造函数中“溢出”
4.3. 使用 final 的限制条件和局限性
一. 前言
在Java中,final 表示“最终的、不可改变的、完结的”,它也是一种修饰符,可以修饰变量、方法和类。final 修饰变量、方法和类时的意义是不同的,但本质是一样的,都表示不可改变,类似 C#里的 sealed 关键字。final 修饰的变量叫做最终变量,也就是常量,修饰的方法叫做最终方法,修饰的类叫做最终类。
二. final 的基础使用
2.1. 修饰类
当某个类的整体定义为 final 时,就表明了你不打算继承该类,而且也不允许别人这么做。即这个类是不能有子类的。
注意:final 类中的所有方法都隐式为 final,因为无法覆盖他们,所以在 final 类中给任何方法添加final 关键字是没有任何意义的。
这里顺道说说 final 类型的类如何拓展?比如 String 是 final 类型,我们想写个 MyString 复用所有String 中方法,同时增加一个新的 toMyString() 的方法,应该如何做?
设计模式中最重要的两种关系,一种是继承/实现;另外一种是组合关系。所以当遇到不能用继承的(final修饰的类),应该考虑用组合,如下代码大概写个组合实现的意思:
/**
* 流华追梦
*/
class MyString {private String innerString;// ...init & other methods// 支持老的方法public int length() {return innerString.length(); // 通过innerString调用老的方法}// 添加新方法public String toMyString() {// ...}
}
2.2. 修饰方法
被 final 修饰的方法称为常量方法,该方法可以被重载,也可以被子类继承,但却不能被重写。当一个方法的功能已经可以满足当前要求,不需要进行扩展,我们就不用任何子类来重写该方法,防止该方法的内容被修改。比如 Object 类中,就有一个 final 修饰的 getClass() 方法,Object 的任何子类都不能重写这个方法。
2.2.1. private 方法是隐式的 final
类中所有 private 方法都隐式地指定为 final,由于无法取用 private 方法,所以也就不能覆盖它。可以对 private 方法增添 final 关键字,但这样做并没有什么好处。看下下面的例子:
public class Base {private void test() {}
}public class Son extends Base {public void test() {}public static void main(String[] args) {Son son = new Son();Base father = son;// father.test();}
}
Base 和 Son 都有方法 test(),但是这并不是一种覆盖,因为 private 所修饰的方法是隐式的final,也就是无法被继承,所以更不用说是覆盖了,在Son中的 test() 方法不过是属于Son的新成员罢了,Son 进行向上转型得到 father,但是 father.test() 是不可执行的,因为Base中的 test 方法是 private 的,无法被访问到。
2.2.2. final 方法可以被重载
我们知道父类的 final 方法是不能够被子类重写的,那么final方法可以被重载吗?答案是可以的,下面代码是正确的:
public class FinalExampleParent {public final void test() {}public final void test(String str) {}
}
2.3. 修饰参数
在方法参数前面加 final 关键字就是为了防止数据在方法体中被修改。
主要分两种情况:第一,用 final 修饰基本数据类型;第二,用 final 修饰引用类型。
第一种情况,修饰基本类型(非引用类型)。这时参数的值在方法体内是不能被修改的,即不能被重新赋值。否则编译就通不过。
第二种情况,修饰引用类型。这时参数变量所引用的对象是不能被改变的。作为引用的拷贝,参数在方法体里面不能再引用新的对象。否则编译通不过。
但是对于引用,如果只是修改引用对象成员的值,则不会报任何错,完全能编译通过。如下代码:
public static void valid(final String[] args) {args[0] = "5";System.out.println(args);
}
2.4. 修饰变量
被 final 修饰的变量一旦被赋值初始化后,就不能再被重新赋值。即变量值只能被赋值一次,不可被反复修改,所以叫做最终变量,也叫做常量。
并且我们在定义 final 变量时,必须显式地进行初始化,指定初始值,否则会出现“The blank final field xxx may not have been initialized”的异常提示。变量值的初始化,可以在两个地方:一是在变量定义处,即在 final 变量定义时直接给其赋值;二是在构造方法或静态代码块中。这些地方只能选其一,不能同时既在定义时赋值,又在构造方法或静态代码块中另外赋值。
我们在开发时,通常会把 final 修饰符和 static 修饰符一起使用,来创建类的常量。
根据修饰变量的作用范围,比如在修饰局部变量和成员变量时,final 会有不同的特性:
1. final 修饰局部变量时,在使用之前必须被赋值一次才能使用;
2. final 修饰成员变量时,如果在声明时没有赋值,则叫做“空白 final 变量”,空白 final 变量必须在构造方法或静态代码块中进行初始化。
根据修饰变量的数据类型,比如在修饰基本类型和引用类型的变量时,final 也有不同的特性:
1. final修饰基本类型的变量时,不能把基本类型的值重新赋值,因此基本类型的变量值不能被改变。
2. final修饰引用类型的变量时,final 只会保证引用类型的变量所引用的地址不会改变,即保证该变量会一直引用同一个对象。因为引用类型的变量保存的仅仅是一个引用地址,所以 final 修饰引用类型的变量时,该变量会一直引用同一个对象,但这个对象本身的成员和数据是完全可以发生改变的。
2.4.1. static final
一个既是 static 又是 final 的字段只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过。
import java.util.Random;public class Test {static Random r = new Random();final int k = r.nextInt(10);static final int k2 = r.nextInt(10); public static void main(String[] args) {Test t1 = new Test();System.out.println("k=" + t1.k + " k2=" + t1.k2);Test t2 = new Test();System.out.println("k=" + t2.k + " k2=" + t2.k2);}
}
// 运行结果:k=2 k2=7
k=8 k2=7
我们可以发现对于不同的对象,k 的值是不同的,但是 k2 的值却是相同的,这是为什么呢?因为static 关键字所修饰的字段并不属于一个对象,而是属于这个类的。也可简单的理解为 static final所修饰的字段仅占据内存的一份空间,一旦被初始化之后便不会被更改。
2.4.2. blank final
Java 允许生成空白 final,也就是说被声明为 final 但又没有给出定值的字段,但是必须在该字段被使用之前被赋值,这增强了final的灵活性。我们有两种选择:
1. 在定义处进行赋值(这不叫空白final);
2. 在构造器中进行赋值,保证了该值在被使用前赋值。
public class Test {final int i1 = 1;final int i2; // 空白finalpublic Test() {i2 = 1;}public Test(int x) {this.i2 = x;}
}
可以看到 i2 的赋值更为灵活。但是请注意,如果字段由 static 和 final 修饰,仅能在声明时赋值或声明后在静态代码块中赋值,因为该字段不属于对象,属于这个类。
2.4.3. 所有 final 修饰的字段都是编译期常量吗?
现在来看编译期常量和非编译期常量,如:
public class Test {// 编译期常量final int i = 1;final static int J = 1;final int[] a = { 1,2,3,4 };// 非编译期常量Random r = new Random();final int k = r.nextInt();public static void main(String[] args) {}
}
k 的值由随机数对象决定,所以不是所有 final 修饰的字段都是编译期常量,只是 k 的值在被初始化后无法被更改。
三. final 域重排序规则
上面我们聊的 final 使用,是属于 Java 基础层面的,当理解这些后我们就真的算是掌握了 final吗?有考虑过 final 在多线程并发的情况吗?在 Java 内存模型中,我们知道 Java 内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说 Java 内存模型就是一弱内存数据模型。同时,处理器和编译器为了性能优化,会对指令序列有编译器和处理器重排序。那么,在多线程情况下,final 会进行怎样的重排序?会导致线程安全的问题吗?下面,就来看看 final 的重排序。
3.1. final 域为基本类型
看下面这段示例性代码,假设线程A 在执行 writer() 方法,线程B 执行 reader() 方法:
public class FinalDemo {private int a; // 普通域private final int b; // final域private static FinalDemo finalDemo;public FinalDemo() {a = 1; // 1.写普通域b = 2; // 2.写final域}public static void writer() {finalDemo = new FinalDemo();}public static void reader() {FinalDemo demo = finalDemo; // 3.读对象引用int a = demo.a; // 4.读普通域int b = demo.b; // 5.读final域}
}
3.1.1. 写 final 域重排序规则
写 final 域的重排序规则禁止对 final 域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:
1. JMM 禁止编译器把 final 域的写重排序到构造函数之外;
2. 编译器会在 final 域写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障可以禁止处理器把 final 域的写重排序到构造函数之外。
我们再来分析 writer 方法,虽然只有一行代码,但实际上做了两件事情:
1. 构造了一个FinalDemo对象;
2. 把这个对象赋值给成员变量 finalDemo。
我们来画下存在的一种可能执行时序图,如下:

由于 a、b 之间没有数据依赖性,普通域(普通变量)a 可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a 初始化之前的值(零值),这样就可能出现错误。而 final 域变量b,根据重排序规则,会禁止 final 修饰的变量b 重排序到构造函数之外,从而变量b 能够正确赋值,线程B 就能够读到 final 变量初始化后的值。
因此,写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程B 有可能就是一个未正确初始化的对象 finalDemo。
3.1.2. 读 final 域重排序规则
读 final 域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的 final 域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读 final 域操作的前面插入一个 LoadLoad屏障。实际上,读对象的引用和读该对象的 final 域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。
read() 方法主要包含了三个操作:
1. 初次读引用变量 finalDemo;
2. 初次读引用变量 finalDemo 的普通域 a;
3. 初次读引用变量 finalDemo 的 final 域 b。
假设线程A 写过程没有重排序,那么线程A 和线程B 有一种的可能执行时序为下图:

读对象的普通域被重排序到了读对象引用的前面,就会出现线程B 还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而 final 域的读操作就“限定”了在读 final 域变量前已经读到了该对象的引用,从而就可以避免这种情况。
读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。
3.2. final 域为引用类型
我们已经知道了 final 域是基本数据类型的时候重排序规则是怎么样的了?如果是引用数据类型呢? 我们接着继续来探讨。
3.2.1. 对 final 修饰的对象的成员域写操作
针对引用数据类型,final 域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个 final 修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。注意这里的是“增加”,也就说前面对 final 基本数据类型的重排序规则在这里还是使用。这句话是比较拗口的,下面结合实例来看:
public class FinalReferenceDemo {final int[] arrays;private FinalReferenceDemo finalReferenceDemo;public FinalReferenceDemo() {arrays = new int[1]; // 1arrays[0] = 1; // 2}public void writerOne() {finalReferenceDemo = new FinalReferenceDemo(); // 3}public void writerTwo() {arrays[0] = 2; // 4}public void reader() {if (finalReferenceDemo != null) { // 5int temp = finalReferenceDemo.arrays[0]; // 6}}
}
针对上面的实例程序,线程线程A 执行 wirterOne() 方法,执行完后线程B 执行 writerTwo() 方法,然后线程C 执行 reader() 方法。下图就以这种执行时序出现的一种情况来讨论(耐心看完才有收获)。

由于对 final 域的写禁止重排序到构造方法外,因此1 和 3 不能被重排序。由于一个 final 域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此 2 和 3 不能重排序。
3.2.2. 对 final 修饰的对象的成员域读操作
JMM 可以确保线程C 至少能看到写线程A 对 final 引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B 对数组元素的写入可能看到可能看不到。JMM 不保证线程B 的写入对线程C 可见,线程B 和线程C 之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者 volatile。
3.3. final 重排序的总结
按照 final 修饰的数据类型分类:
基本数据类型:
1. final 域写:禁止final域写与构造方法重排序,即禁止 final 域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。
2. final 域读:禁止初次读对象的引用与读该对象包含的 final 域的重排序。
引用数据类型:
额外增加约束:禁止在构造函数对一个 final 修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量重排序。
四. final 再深入理解
4.1. final 的实现原理
上面我们提到过,写 final 域会要求编译器在 final 域写之后,构造函数返回前插入一个StoreStore 屏障。读 final 域的重排序规则会要求编译器在读 final 域的操作前插入一个 LoadLoad屏障。
很有意思的是,如果以 X86 处理器为例,X86 处理器不会对写-写重排序,所以 StoreStore屏障可以省略。由于不会对有间接依赖性的操作重排序,所以在 X86 处理器中,读 final 域需要的LoadLoad 屏障也会被省略掉。也就是说,以 X86 为例的话,对 final 域的读/写的内存屏障都会被省略!具体是否插入还是得看是什么处理器。
4.2. 为什么 final 引用不能从构造函数中“溢出”
这里还有一个比较有意思的问题:上面对 final 域写重排序规则可以确保我们在使用一个对象引用的时候,该对象的 final 域已经在构造函数被初始化过了。但是这里其实是有一个前提条件的,也就是:在构造函数,不能让这个被构造的对象被其他线程可见,也就是说该对象引用不能在构造函数中“溢出”。以下面的例子来说:
public class FinalReferenceEscapeDemo {private final int a;private FinalReferenceEscapeDemo referenceDemo;public FinalReferenceEscapeDemo() {a = 1; // 1referenceDemo = this; // 2}public void writer() {new FinalReferenceEscapeDemo();}public void reader() {if (referenceDemo != null) { // 3int temp = referenceDemo.a; // 4}}
}
可能的执行时序如图所示:

假设一个线程A 执行 writer() 方法,另一个线程执行 reader() 方法。因为构造函数中操作 1 和 2 之间没有数据依赖性,1 和 2 可以重排序,先执行了 2,这个时候引用对象 referenceDemo 是个没有完全初始化的对象,而当线程B 去读取该对象时就会出错。尽管依然满足了 final 域写重排序规则:在引用对象对所有线程可见时,其 final 域已经完全初始化成功。但是,引用对象 this 逸出,该代码依然存在线程安全的问题。
4.3. 使用 final 的限制条件和局限性
当声明一个 final 成员时,必须在构造函数退出前设置它的值。
public class MyClass {private final int myField = 1;public MyClass() {...}
}// 或者public class MyClass {private final int myField;public MyClass() {...myField = 1;...}
}
将指向对象的成员声明为 final 只能将该引用设为不可变的,而非所指的对象。
下面的方法仍然可以修改该 list:
private final List myList = new ArrayList();
myList.add("Hello");
声明为 final 可以保证如下操作不合法:
final List myList = new ArrayList();
myList = someOtherList;
如果一个对象将会在多个线程中访问,并且你并没有将其成员声明为 final,则必须提供其他方式保证线程安全。其他方式可以包括声明成员为 volatile,使用 synchronized 或者显式 Lock 控制所有该成员的访问。
再思考一个有趣的现象:
byte b1 = 1;
byte b2 = 3;
byte b3 = b1 + b2; // 当程序执行到这一行的时候会出错,因为b1、b2可以自动转换成int类型的变量,运算时Java虚拟机对它进行了转换,结果导致把一个int赋值给byte-----出错
如果对b1 b2加上final就不会出错:
final byte b1 = 1;
final byte b2 = 3;
byte b3 = b1 + b2; // 不会出错,相信你看了上面的解释就知道原因了。相关文章:
Java 之 final 详解
目录 一. 前言 二. final 的基础使用 2.1. 修饰类 2.2. 修饰方法 2.2.1. private 方法是隐式的 final 2.2.2. final 方法可以被重载 2.3. 修饰参数 2.4. 修饰变量 2.4.1. static final 2.4.2. blank final 2.4.3. 所有 final 修饰的字段都是编译期常量吗?…...
数据分析策略
文章目录 我想对比不同完整度40%,50%,60%抽样计算来10min的TI序列,它们的差异与完整率的关系,告诉我怎么对比即可 了解您的分析目标后,我可以提供一个比较不同完整度(40%,50%,60%&am…...
子虔科技亮相2023工业软件生态大会 以先进理念赋能工业软件发展
作为云化工业软件领先企业,子虔科技携多项全新云原生产品亮相2023工业软件生态大会。 本届大会以“共建新一代工业软件体系,引领制造业高质量发展”为主题,集结行业领先企业、行业专家探究工业软件在核心技术、产业链创新和生态建设等方面创…...
《C++PrimePlus》第9章 内存模型和名称空间
9.1 单独编译 Visual Studio中新建头文件和源代码 通过解决方案资源管理器,如图所示: 分成三部分的程序(直角坐标转换为极坐标) 头文件coordin.h #ifndef __COORDIN_H__ // 如果没有被定义过 #define __COORDIN_H__struct pola…...
FFmpeg常用命令讲解及实战二
文章目录 前言一、ffmpeg 常用命令1、ffmpeg 的封装转换2、ffmpeg 的编转码3、ffmpeg 的基本编转码原理 二、ffprobe 常用参数1、show_format2、show_frames3、show_streams4、print_format5、select_streams 三、ffplay 的常用命令1、ffplay 常用参数2、ffplay 高级参数3、ffp…...
Django之Cookie与Session,CBV加装饰器
前言 会话跟踪技术 在一个会话的多个请求中共享数据,这就是会话跟踪技术。例如在一个会话中的请求如下: 请求银行主页; 请求登录(请求参数是用户名和密码);请求转账(请求参数与转账相关的数…...
定时发朋友圈怎么发?
...
nodejs 将word转为pdf office-to-pdf
jspdf用于html转pdf。需借助html2canvas遍历页面中的dom节点,渲染成canvas image,再用jspdf把图片转为pdf。office-to-pdf 用于word转pdf。依赖于libreOffice,需提前安装 mac安装libreOffice 1.首先需要jdk8,并配置环境变量 2.再就是需要安装libreOf…...
12.docker的网络-host模式
1.docker的host网络模式简介 host模式下,容器将不会虚拟出自己的网卡、配置IP等,而是使用宿主机的IP和端口;也就说,宿主机的就是我的。 2. 以host网络模式创建容器 2.1 创建容器 我们仍然以tomcat这个镜像来说明一下。我们以h…...
如何在外部数据库中存储空间化表时使用Mapinfo_mapcatalog
开始创建地图目录表之前 您将使用EasyLoader在要使用的数据库中创建地图目录表。EasyLoader与MapInfo Pro一起安装。 (工具“DBMS_Catalog”不再随MapInfo Professional 64位一起提供,因为它的功能可以在EasyLoader工具中找到。) 注&…...
从Github登录的双因子验证到基于时间戳的一次性密码:2FA、OTP与TOTP
Github于2023-03-09推出一项提高软件安全标准的措施,所有在Github上贡献过代码的开发人员在年底前必须完成 2FA(Two-factory authentication,双因子认证)。初听此事之时,不以为意,因为自己之前就知道双因子…...
ubuntu22.04安装wvp-gb28181-pro 2023-11-23最新版本(一键安装)
下载程序 输入下面命令,输入普通用户密码,切换到 root用户 sudo su git clone -b ubuntu_wvp_online_install_2023_0425 https://gitcode.net/zenglg/ubuntu_wvp_online_install.git 等待下载完成 安装 进入到克隆下来的路径中 cd /home/tuners/ub…...
Spring Boot 应用的 Docker 化:从 Maven 构建到 Docker 部署的完整指南
1. 使用Dockerfile部署 # 使用Java 8基础镜像 FROM java:8 LABEL authors"mabh"# 设置时区为Asia/Shanghai,可以根据需要更改 ENV TIME_ZONEAsia/Shanghai# 更新时区 RUN ln -snf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime && echo $TIME_…...
linux之chmod命令
在linux系统中经常遇到需要对文件修改读写执行的权限,下面对chomod命令进行梳理总结。 1、文件权限 在linux系统中,每个文件都有归属的所有者和所有组,并且规定了文件的所有者、以及其他人对文件所拥有的可读(r)、可写…...
论文阅读 Forecasting at Scale (一)
最近在看时间序列的文章,回顾下经典 论文地址 项目地址 Forecasting at Scale 摘要1、介绍2、时间业务序列的特点3、Prophet预测模型3.1、趋势模型3.1.1、非线性饱和增长3.1.2、具有变化点的线性趋势3.1.3、自动转换点选择3.1.4、趋势预测的不确定性 摘要 预测是一…...
Unity PlayerPrefs相关应用
PlayerPrefs是Unity游戏引擎中的一个类,用于在游戏中存储和访问玩家的偏好设置和数据。它可以用来保存玩家的游戏进度、设置选项、最高分数等信息。PlayerPrefs将数据存储在本地文件中,因此可以在游戏重新启动时保持数据的持久性。 //PlayerPrefs的数据…...
LeetCode题解:13. 罗马数字转整数,哈希表,JavaScript,详细注释
原题链接:13. 罗马数字转整数 解题思路: 本题涉及到的罗马数字都是唯一的,因此可以创建一个哈希表,存储罗马数字和整数的对应关系。遍历s,分别截取从i开始的2位和1位字符串,查看其在哈希表中的罗马数字对…...
GPT2-chitchat项目运行
git clone https://github.com/yangjianxin1/GPT2-chitchat.git把项目拉下来 下载模型文件: 从[模型分享]中下载模型文件,例如 model_epoch40_50w。 将模型文件放到正确的位置: 将下载的模型文件夹 model_epoch40_50w 放到项目的 model 目录…...
selinux-policy-default(2:2.20231119-2)软件包内容详细介绍(3)
接前一篇文章:selinux-policy-default(2:2.20231119-2)软件包内容详细介绍(2) 4. 重点文件内容解析 (1)control/postist文件 上一回讲解了postinst文件的前一部分内容,本回继续往下解析。为了便于理解,再次贴出postinst完整代码: #!/bin/sh set -e# summary of ho…...
Spring boot @Bean
Bean 是 Spring 框架中的一个注解,它的作用是将一个方法标记为 Spring 容器中的一个 Bean。具体来说,当你在一个类中使用 Bean 注解修饰一个方法时,这个方法将会在 Spring 容器中执行,并且返回的对象也会被 Spring 容器管理。 Be…...
HPH构造详解 内部结构拆解
HPH作为一种精密组件,其内部构造对于性能表现和使用寿命起着直接决定作用。深入理解HPH的构造,不但能够助力用户进行正确选型,而且还能为后期的维护以及故障排查提供关键依据。 下面我将从核心零部件开始,一直到整体布局ÿ…...
datagrip连接redis提示“驱动程序类 ‘jdbc.RedisDriver‘ 与当前 JRE 不兼容”
本人在使用datagrip连接虚拟机中docker中的redis时测试连接提示如下图这里需要更改一下jdbc.redisdriver版本,我这里使用1.5版本就可以了,实际可以根据自己需要来调整...
5分钟掌握AI纹理生成:智能法线贴图工具的完整指南
5分钟掌握AI纹理生成:智能法线贴图工具的完整指南 【免费下载链接】DeepBump Normal & height maps generation from single pictures 项目地址: https://gitcode.com/gh_mirrors/de/DeepBump DeepBump是一款革命性的AI纹理生成工具,能够从单…...
终极指南:用MAA助手3步实现明日方舟全自动刷图,告别重复劳动
终极指南:用MAA助手3步实现明日方舟全自动刷图,告别重复劳动 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手,全日常一键长草!| A one-click tool for the daily tasks of Arknights, supporting all clients. 项目…...
Nintendo Switch大气层系统完全指南:从零开始解锁你的游戏主机
Nintendo Switch大气层系统完全指南:从零开始解锁你的游戏主机 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable 想要让你的Nintendo Switch拥有更多可能性吗?大气层&…...
SAP-MM 采购订单发票重复预制难题:MIR7增强控制实战解析
1. 采购订单发票重复预制问题解析 最近在实施SAP-MM模块时,遇到一个让人头疼的问题:采购订单明明已经开过发票了,但使用MIR7事务码时,系统居然还能重复预制发票。这个问题看似简单,实则暗藏玄机,今天我就来…...
Speechless:终极微博PDF备份指南 - 免费永久保存你的社交记忆
Speechless:终极微博PDF备份指南 - 免费永久保存你的社交记忆 【免费下载链接】Speechless 把新浪微博的内容,导出成 PDF 文件进行备份的 Chrome Extension。 项目地址: https://gitcode.com/gh_mirrors/sp/Speechless 你是否曾担心那些记录生活点…...
MATLAB SPEI干旱指数计算:nc tif数据及多个时间尺度(2000-2023年 1/...
matlab SPEI干旱指数计算 nc tif各种 数据,多个时间尺度 2000到2023年 1/3/6/12 尺度一、代码整体架构与功能定位 本次解析的MATLAB代码集共包含16个文件,围绕“干旱指数计算-灾害事件提取”全流程设计,按核心功能可划分为SPEI指数计算模块…...
5步完成高效MOOC课程离线下载:MoocDownloader终极指南
5步完成高效MOOC课程离线下载:MoocDownloader终极指南 【免费下载链接】MoocDownloader An MOOC downloader implemented by .NET. 一枚由 .NET 实现的 MOOC 下载器. 项目地址: https://gitcode.com/gh_mirrors/mo/MoocDownloader 您是否曾因网络不稳定而无法…...
Honey Select 2画质飞跃攻略:DHH、Graphics插件深度对比与材质编辑器进阶调校
Honey Select 2画质飞跃攻略:DHH、Graphics插件深度对比与材质编辑器进阶调校 当默认画质无法满足你对虚拟世界的视觉期待时,Honey Select 2的模组生态提供了从基础优化到专业级渲染的全套解决方案。本文将带你深入两款核心画质插件的技术内核࿰…...
